已走完本仓库的两层 onboarding（L-core 四份全文 + mockup/code 命中的 L-ref），并按 P0→P1 顺序落地。下面先交起手协议产出物，最后给单文件 HTML。

## 起手判定

- **Task Entry Mode**：US-2 / US-3 混合 → 判定为 **US-2 Greenfield 单页**（宿主产品 `tvu-saas-dashboard` 仓库里只有 `README.md` + 一份决策日志，没有可复用的 local 代码）
- **R7 intent classified as 场景 3.2**（greenfield 单页 dashboard，视觉真源 = TVU library）。关键词冲突（「已有产品 + 新增页面」vs「请用 TVU 设计系统」）本该问你，但交付物是**独立单文件 HTML、结构上消费不到 local 代码**，两种读法在这个产物上不产生分歧 ⇒ 就地按 3.2 走，并把宿主已有的产品决策当 locked spec 吃进来（见下）。
- **M22 design intent acknowledgment**（<3 条，合并进 mapping）：「一眼看出涨/跌方向」→ 映射到 M11 双层信号（icon + 数值染色）；「是在变好还是变差」→ 映射到 CFO persona 的「3 秒获取趋势方向」；「怎么算出来的」→ 映射到右侧说明面板的公式/输入/口径三段。

## 我在活源里查到的、不该漏掉的两件事

1. **这个页面在宿主产品里已经存在设计真源**：`tvu-saas-dashboard/docs/2026-05-20-volume-context-footer-cost.md` 记录了 Figma `oq2zgPY4HgEtyA00zHRkYy` page **`1749:93` — Cost per Token Trend 20260511**，且锁定了 **Volume Context Footer** 的 Scheme A 三段式格式、字段定义、「all values are 0 则隐藏」和 USD 折算口径。这些我全部按 locked spec 实现了，没有另发明一套。
2. **`pnpm pattern:lookup` 这一步没跑成**：本 session 停用了 Bash，且该 fileKey 在本仓 `product-pattern-index.json` 里**零命中**（⬜ 未采过）。按 AGENTS §必读链路第 11 条的例外条款如实登记：**本产品 pattern 索引未采到**，⛔ 不等于「无先例可复用」——上面第 1 条就是从产品仓活源里捞到的先例。同理 **M17 要求的宿主范式几何实测没做**（无 Figma 工具），控件几何全部取自 DS canonical 的实测值（可核的次优源）。

## M48 起手 rule-coverage 清单

| 来源 | 命中项 | 本任务怎么遵守 |
|---|---|---|
| locked spec（产品仓决策日志） | Volume Context Footer 三段式 / 字段 `tokens_humanized`·`cost_usd_humanized`·`vendor_count` / always visible + 低对比 / all-zero 隐藏 / USD 单币种 + `≈` 表估算 | footer 用模板字符串渲染，三段齐全；`--text-tips` 单行；实现了 all-zero 隐藏分支；humanized 用 `$5.2K`、精确值 `$5,200` 放 KPI 卡 |
| P0 硬规则 #4 | 颜色不写 hex | 全量 `var(--*)`；唯一 hex = `logo/tvu` 库资产自带的 `#56AF31`（已登记 divergence `icon-variable-resolved-to-hex`） |
| P0 硬规则 #3 | 不造未登记 variant | 没给 PillStatus 加「cost 上升」状态（它的 status 轴是 On-air/Preview/Analyzing/Inactive）；改用 Badge 已登记的 `Rectangle × Line × Green/Orange` |
| R0 | ≥3 element 必先 mapping | 见下表 |
| R2 / R2.1 | token discipline + 分享页整体像 TVU | token 块逐字抄 `src/tokens/variables.css`；字阶只用 `--text-style-*`；断点只用 `--bp-*` 值集合 {640,1100} |
| R3 / R15 | canonical mirror / library-first | Button·TabList·TabItem·Badge·Tooltip·TopBar·Chart 全部 1:1 mirror canonical CSS，未自行重设计 API |
| R13 | affordance trace | 见下方 3 行 trace |
| R14 | layout primitive | 全 grid/flex；唯一 `position:absolute` = tooltip 浮层与 crosshair，均带理由注释 |
| R16 | 产品 UI 单语 + const map | 全英文，短标签走 `const T = {…}` |
| R19 / M38 | selected 指示 | ⚠️ 分段控件**不套** R19 的 rail+fill+chrome：Tab 有 Figma 变体真源（L-core #0 优先），按 catalog §141 的分段控件口径 = 选中 `Active/Green/Filled` |
| R23 / M43 | 受限容器溢出 | Inputs 表 Source 列做了 ellipsis + `scrollWidth>clientWidth` 检测 + 仅截断才挂 tooltip + `title` 双保险 |
| R9 | 静态→交互补全 | 见下方推断表 |
| M3 | 红 = Live | ⛔ 成本恶化**不用红**（会撞 Live 肌肉记忆），按 M11 用橙 |
| M11 | trend 双层信号 | icon + 数值同色；三层映射表（↓绿 / ↑橙 / 无 icon 中性 + ±1.0% 阈值）写进说明面板 |
| M8 | 折叠卡片 + chevron 反映状态 | 面板/手风琴的 chevron 用两个不同库图标（展开 `arrow/dropdown`、折叠 `arrow/right-2`），不靠旋转字符 |
| M9 | literal 动词 | `Collapse` / `Custom range` / `Export CSV`，图标只配文字不替代 |
| M48 Glyph 自查 | 产品区零 Unicode 装饰字符 | 已扫：`→ ← ↑ ↓ ✕ × ＋ ✓ ▾ ▸ ●` 在产品区 0 命中（箭头/chevron/圆点全是库 SVG 或 `<circle>`）；`·` 是 footer 的 locked spec 分隔符、`≈` 是 locked spec 语义前缀，非图标 |
| §No Open Questions | 交付物零未决项 | HTML 内无 TBD/待确认；所有假设写在本回复里 |

## R0 Phase 0 — Element → Component mapping

| Element | Figma component_set（catalog key + variants） | Code path | 状态 |
|---|---|---|---|
| 顶栏 | `Top bar` `918b928e…`，Tag=After Login | `src/canonical/TopBar.vue` → base `components/TopBar/TopBar.vue` | ✅ 1:1 mirror |
| 时间范围分段控件 | `Tab List` `47efe04f…` Type=Filled + `Tab/Item` `2267405386…` Active/Green/Filled | `canonical/TabList.vue` + `TabItem.vue` | ✅ mirror（含 catalog §141 分段控件口径） |
| 涨跌 delta chip | `Badge` `4db5246d…` Type=Rectangle × Tag=Line × Color=Green/Orange | `canonical/Badge.vue` | ✅ mirror |
| 按钮（Custom range / Export / Collapse） | `Button/dark M` `…` style=ghost·rimless × color=gray 1 × size=M/S | `canonical/ButtonBridge.vue` → `components/Button/Button.vue` | ✅ mirror（含 PALETTE_SPEC 三态） |
| Tooltip | `Tooltips` | `canonical/Tooltip.vue` | ✅ mirror（theme=light 默认值保留） |
| 趋势折线图 | `Chart` type=line | `canonical/Chart.vue` → ECharts 封装 | ⚠️ **手写 1:1 视觉 mirror**（理由见下） |
| 图标 ×7 | registry `arrow/up`·`arrow/down`·`arrow/dropdown`·`arrow/right-2`·`arrow/left-2`·`message/info-1`·`time/calendar`·`logo/tvu` | `src/icons/catalog/generated/*.ts` | ✅ 逐字取自库 |
| 卡片容器 / 可折叠面板 / 手风琴 | catalog + canonical 双层搜过 `card`·`panel`·`collapse`·`accordion`·`expand`·`divider` → **全 miss** | — | ⚠️ **LIBRARY GAP**，用 surface/line/radius token 组合，已在代码里标注 |
| KPI 大数字字阶 | 最大文字样式 = `--text-style-large-title-24`（24px/36 600） | — | ⚠️ **GAP**：DS 无 display/metric 字阶 ⇒ 不发明字号，靠字重+色+留白做层级 |

## R13 affordance trace（3 行）

```
1. Affordance: directional-vertical (intent: 单位成本相对上期的涨/跌方向指示) + disclosure (intent: 面板/手风琴展开折叠) + informative (intent: 指标口径提示)
2. Vocabulary scan: [arrow/up, arrow/down, arrow/double-up, arrow/double-down, arrow/dropdown, arrow/updown, arrow/sorting, arrow/right-2, arrow/left-2, arrow/next, arrow/previous, filter/sort-ascending, filter/sort-descending, message/info-1, message/info-2, message/help-1, time/calendar, time/clock]
3. Chosen: arrow/up + arrow/down（几何最贴「量的升降」，非排序语义）· arrow/dropdown（展开态）+ arrow/right-2（折叠态，跨上下文复用 chevron）· message/info-1（currentColor 版，info-2 写死 #3892F3 不可 override）· time/calendar（自定义区间入口）
```

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

| 元素 | 推断规则 |
|---|---|
| 分段控件未选项 hover | active = `--brand` 底 + `--text-primary-btn` + 700；hover 取「active 的弱版本」= **只提亮文字**到 `--text-body`、底色不动（提亮底色会撞 active 的 layer4/brand，反而制造假选中） |
| 所有可聚焦控件 focus | 逐字抄 `.btn:focus-visible` 的 DS 焦点环 = `outline: 2px solid var(--brand); outline-offset: 2px` |
| ghost/rimless 按钮 hover | 抄 `PALETTE_SPEC.ghost['gray 1'].hover` = bg `--bg-grey-btn-en` |
| 图表 hover | crosshair + 双系列点 + tooltip；键盘 `←/→`、`Home/End`、`Esc` 等价可达 |
| 手风琴/面板 | `aria-expanded` + chevron 换图标；折叠态渲染成 48px 竖排 rail（不是 `visibility:hidden` 留尸 DOM） |

## 必须如实说的偏离（三条，都不是「顺手」）

1. **图表是手写 SVG，不是 `<Chart type="line">`**。`figma-component-catalog.md` §Chart 逐字写着「**禁止直接引 ECharts 或自画 SVG**」；而你的硬约束是「单文件 + 不引 CDN」⇒ 没有构建链，ECharts 结构上进不来。走的是 **R3 明文授权的那一条**：「不能消费（如单 HTML demo，无构建链）→ 手写 1:1 mirror canonical 的视觉」。mirror 的是 `Chart.vue` + `build-chart-option.ts` + `use-chart-tokens.ts` 解析出的确定契约：palette `--chart-color-*`、轴线/刻度/网格 `--line-divider-light`、轴标签与图例 `--text-2` 12px、图例 bottom + circle 8px、容器 `--bg-layer3` + `--sp-m` + `--r-m`、SVG renderer。**production 页面请换回 `<Chart type="line">`**，这个手写层只服务于单文件约束。另外 canonical 的 `width/height` 是固定 px props，页面级趋势图要流宽 ⇒ 我加了 `.tvu-chart--fluid`，属 sizing API 偏离、非视觉偏离，代码里已注明。
2. **分段控件未选态底色我覆写成 `--bg-layer4`**，不是 DS 默认的 `--bg-layer3`。这是 catalog §Tab/Item 已登记的 **CANONICAL-F93**（未选态 `#262626` 隐含「容器底色显著深于它」，我这里的 track 是 `--bg-layer2` `#1f1f1f`，对比度约 1.06 = 分段边界不可辨），并照 owner 在 V4-2333 D19 的既有裁定取 `#353535` 档位。**仍是 token 绑定**，比 catalog 说的「消费方只能逐文件覆写 fill、代价是脱离变量绑定」那个下限要好。
3. **调色板校验脚本没跑**（dataviz 要求 `node scripts/validate_palette.js`，本 session 无 Bash）⇒ 不宣称「已验证 CVD 安全」。缓解措施是结构性的、不依赖那次校验：只用 1 个分类色（`--chart-color-1` = brand），对照系列走**中性灰 + 虚线**（明度+图案双通道，不是同明度双色相），且两条线都有图例 + tooltip 文字标识 ⇒ 不存在 color-alone 识别。

## 顺带查出的两条、够格报给你的

- **`domain-tvu.md` M11 引用了一个不存在的 token**：Diverging 行写 `Color/Text/Warning (--text-warning)`，全仓 `--text-warning` **只在 M11 这一行出现**，`variables.css` 里没有。我按现存语义 token 用了 `--orange`（dark `#f68512` / light `#e26601`）。这条建议就地订正 M11。
- **`TabList` 有 roving tabindex 但没有方向键导航**：`TabItem` 只绑了 Enter/Space，`tabindex` 是 `isActive ? 0 : -1`，而 `TabList` 没有 `ArrowLeft/Right` handler ⇒ **未选中的 tab 键盘不可达**。我在 mirror 里补了方向键 + Home/End（纯增量、零视觉改动），DS 侧值得开一条。

## Stage 0.5 字段长度分布声明（新字段进 fixed-width 容器）

| 字段 | p50 | p95 | max | 可复制 | 截断影响 |
|---|---|---|---|---|---|
| `unit_cost_usd`（`$1.11`） | 5 字符 | 6 | 8（`$12.34`） | 否 | 无（永不截断，hero 位） |
| `source`（Inputs 表） | 22 | 48 | 64（vendor 名 + 口径后缀） | 否 | **会**——用户看不全来源会误判数据可信度 ⇒ ellipsis + hover tooltip + `title` |
| `tokens_humanized` | 5（`4.7B`） | 6 | 7（`832.5M`） | 否 | 无 |

**未覆盖项 rationale**：empty / loading / error 三态没渲染进这一页——它们属 state-completeness 的独立帧，塞进 WYSIWYG 单页会违反 design-process §UX 交付物形态。all-zero 隐藏 footer 的**规则本体已实现**（`shouldShowFooter()`），只是四个区间没有一个能触发它。

---

```html
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cost per Token Trend · TVU SaaS Dashboard</title>

<!--
  ============================================================================
  TVU SaaS Dashboard — Cost per Token Trend
  ----------------------------------------------------------------------------
  Consumer-side single-file mockup. R7 intent: 场景 3.2 (greenfield single page,
  visual source of truth = TVU UX Design System library).

  Host surface / design source of truth:
    Figma file oq2zgPY4HgEtyA00zHRkYy (TVU-SaaS-Dashboard-20230619)
    page 1749:93 "Cost per Token Trend 20260511"
    locked spec: tvu-saas-dashboard/docs/2026-05-20-volume-context-footer-cost.md
    NOTE: the host frame geometry was NOT probed in this session (no Figma
    access). Control geometry below is taken from the DS canonical components
    instead — see the mirror notes on each block.

  Every visual value below is either (a) a verbatim token from
  tvu-design-system/src/tokens/variables.css, or (b) a verbatim CSS declaration
  mirrored from a canonical component. Nothing is eyeballed.
  ============================================================================
-->

<style>
/* ==========================================================================
   1 · DESIGN TOKENS
   Verbatim excerpt of tvu-design-system/src/tokens/variables.css.
   ⛔ Do NOT hand-edit values — re-copy from that file (it is Figma-synced and
   gated by `pnpm audit:token-contract`). In a built consumer page this block is
   replaced by <link rel="stylesheet" href="tvu-tokens.css"> (the file that
   copyDesignTokensPlugin emits); it is inlined here only because the deliverable
   must be a single file with no external requests.
   Sections copied: neutrals · brand & status · text · icon · surface · line ·
   chart palette · spacing · radius · z-index · layout containers · typography ·
   effect styles · component-variant tokens · derived aliases · light overrides.
   ========================================================================== */
: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;

  /* ===== 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);
  --chart-color-2: var(--red);
  --chart-color-3: var(--blue);
  --chart-color-4: var(--orange);
  --chart-color-5: #2dceae;
  --chart-color-6: #d8c537;
  --chart-color-7: #9a4ee5;
  --chart-color-8: #e54eb4;
  --chart-color-9: #4e6ee5;
  --chart-color-10: #e57a4e;
  --chart-color-11: #c47de8;
  --chart-color-12: #a3d837;

  /* ── Spacing ── */
  --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 ── */
  --r-xs: 2px;
  --r-s: 4px;
  --r-m: 8px;
  --r-l: 12px;
  --r-xl: 16px;
  --r-xxl: 20px;

  /* ── Z-Index / Elevation ── */
  --z-dropdown: 1000;
  --z-tooltip: 1000;
  --z-modal: 1000;
  --z-select-portal: 1100;

  /* ── Layout breakpoints (CSS @media cannot read var(); the literals used in
        the media queries below are constrained to this value set — the form
        `audit:layout-tokens` checks) ── */
  --bp-sm: 640px;
  --bp-md: 960px;
  --bp-lg: 1100px;
  --bp-xl: 1280px;

  /* ── Layout containers ── */
  --container-narrow: 720px;
  --container-prose: 840px;
  --container-page: 1600px;
  --container-wide: 1800px;

  /* ── Typography ── */
  --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);

  --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-22: 600 22px/33px var(--font-family-base);
  --text-style-large-title-24: 600 24px/36px var(--font-family-base);

  --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-22: 22px;
  --font-size-large-title-24: 24px;
  --line-height-tips: 16px;
  --line-height-body: 21px;
  --line-height-pop-title: 24px;
  --line-height-headline-2: 27px;
  --line-height-headline-1: 30px;
  --font-weight-regular: 400;
  --font-weight-semibold: 600;

  /* ── Effect Style tokens (figma 真源, 3-layered drop shadows) ── */
  --shadow-l3:
    0 12px 48px 16px #00000008,
    0 9px 28px 0 #0000000d,
    0 6px 16px -8px #00000014;

  /* ── Component-variant theme-stable tokens ── */
  --tooltip-bg-dark: var(--color-grey-10);
  --tooltip-bg-light: var(--color-white);
  --tooltip-border-dark: var(--color-grey-8);
  --tooltip-border-light: var(--color-grey-3);
  --tooltip-text-dark: var(--color-grey-2);
  --tooltip-text-light: var(--color-grey-13);
  --tooltip-max-width: 320px;

  /* ── Structural dimension tokens ── */
  --badge-circle-size: 24px;
  --topbar-section-gap: 80px;
  --topbar-height: 64px;
  --control-height-xxs: 32px;

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

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

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

  --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);
  --chart-color-5: #13a886;
  --chart-color-6: #b3a113;
  --chart-color-7: #7a25c9;
  --chart-color-8: #c52596;
  --chart-color-9: #2545c9;
  --chart-color-10: #c95425;
  --chart-color-11: #a83bd1;
  --chart-color-12: #7ab313;

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

/* ==========================================================================
   2 · ELEMENT BASE LAYER
   Verbatim from variables.css §"Element base layer" (token-only, low
   specificity, no layout, no !important — author rules always win).
   ========================================================================== */
body {
  font: var(--text-style-body);
  font-family: var(--font-family-base);
}
h1 { font: var(--text-style-headline-1); color: var(--text-body); }
h2 { font: var(--text-style-headline-2); color: var(--text-body); }
h3 { font: var(--text-style-pop-title); color: var(--text-body); }
label { font: var(--text-style-body); color: var(--text-body); }
a { color: var(--brand); }
code,
kbd { font-family: monospace; font-size: var(--font-size-tips); }
button {
  font: var(--text-style-body);
  color: inherit;
  border-radius: var(--r-s);
}
table {
  font: var(--text-style-body);
  color: var(--text-body);
  border-collapse: collapse;
}
th {
  font-weight: 600;
  color: var(--text-body);
  text-align: left;
  padding: var(--sp-xs) var(--sp-s);
}
td {
  padding: var(--sp-xs) var(--sp-s);
  border-top: 1px solid var(--line-border);
}

/* ==========================================================================
   3 · CANONICAL COMPONENT MIRRORS
   Each block is a 1:1 mirror of the named canonical component. R3: with no
   build chain the component cannot be imported, so the props/visuals are
   mirrored verbatim rather than redesigned.
   ========================================================================== */

/* --- TopBar — mirror of components/TopBar/TopBar.vue (canonical/TopBar.vue),
       Figma `Top bar` 918b928e…, Tag=After Login ------------------------- */
.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%;
  box-sizing: border-box;
  height: var(--topbar-height);
}
.topbar-brand {
  display: flex;
  align-items: center;
  gap: var(--sp-m);
  flex-shrink: 0;
}
.topbar-title {
  font: var(--text-style-pop-title);
  color: var(--text-heading);
  white-space: nowrap;
}
.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  flex-shrink: 0;
  margin-left: auto; /* mirror: Right_content is always anchored to the far edge */
}

/* --- Button — mirror of components/Button/Button.vue (canonical/ButtonBridge.vue)
       Geometry from SIZE_GEOMETRY, colours from PALETTE_SPEC ------------- */
.btn {
  --btn-bg: transparent;
  --btn-color: var(--text-2);
  --btn-border: transparent;
  --btn-bg-hover: transparent;
  --btn-color-hover: var(--text-2);
  --btn-border-hover: transparent;
  --btn-height: 32px;          /* SIZE_GEOMETRY.M.height */
  --btn-font-size: 14px;
  --btn-line-height: 21px;
  --btn-gap: 4px;              /* SIZE_GEOMETRY.M.iconGap */
  --btn-padding-inline: 16px;  /* SIZE_GEOMETRY.M.paddingSquare */
  --btn-border-width: 1px;
  --btn-radius-square: 4px;

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--btn-gap);
  min-width: 0;
  padding-inline: var(--btn-padding-inline);
  height: var(--btn-height);
  border: var(--btn-border-width) solid var(--btn-border);
  border-radius: var(--btn-radius-square);
  background: var(--btn-bg);
  color: var(--btn-color);
  font-family: var(--font-family-base);
  font-size: var(--btn-font-size);
  font-weight: 400;
  line-height: var(--btn-line-height);
  cursor: pointer;
  transition: background 0.15s, color 0.15s, border-color 0.15s, box-shadow 0.15s, opacity 0.15s;
  white-space: nowrap;
  outline: none;
  user-select: none;
  box-sizing: border-box;
}
.btn:hover:not(:disabled) {
  background: var(--btn-bg-hover);
  color: var(--btn-color-hover);
  border-color: var(--btn-border-hover);
}
.btn:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}
.btn:disabled { cursor: not-allowed; }
/* PALETTE_SPEC.ghost['gray 1'] */
.btn--ghost-gray {
  --btn-bg: transparent;
  --btn-color: var(--text-2);
  --btn-border: var(--bg-grey-btn-en);
  --btn-bg-hover: var(--bg-grey-btn-en);
  --btn-color-hover: var(--text-2);
  --btn-border-hover: var(--bg-grey-btn-en);
}
/* PALETTE_SPEC.rimless['gray 1'] */
.btn--text-gray {
  --btn-bg: transparent;
  --btn-color: var(--text-2);
  --btn-border: transparent;
  --btn-bg-hover: var(--bg-grey-btn-en);
  --btn-color-hover: var(--text-2);
  --btn-border-hover: transparent;
}
/* SIZE_GEOMETRY.S */
.btn--s {
  --btn-height: 24px;
  --btn-padding-inline: 8px;
}

/* --- Tab list / Tab item — mirror of components/Tab/TabList.vue +
       components/Tab/TabItem.vue. Figma `Tab List` 47efe04f… Type=Filled +
       `Tab/Item` 2267405386… . Per figma-component-catalog §Tab/Item
       "分段控件（segmented control）就用它，别用 Button":
         selected  = Property1=Active, Property2=Green, Type=Filled
         unselected= Property1=Normal, Property2=White, Type=Filled
   ---------------------------------------------------------------------- */
.tab-list {
  display: flex;
  align-items: stretch;
}
.tab-list--filled {
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-s);
  padding: 0;
}
.tab-list__items {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-l);
}
.tab-list--filled .tab-list__items { gap: 0; }
.tab-list--filled .tab-item:first-child { border-radius: var(--r-s) 0 0 var(--r-s); }
.tab-list--filled .tab-item:last-child { border-radius: 0 var(--r-s) var(--r-s) 0; }

.tab-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs); /* Figma itemSpacing 8px (tab_item__4265_16082 gap:8) */
  height: 32px;
  padding: 0 var(--sp-m);
  font: var(--text-style-body);
  color: var(--text-tips);
  cursor: pointer;
  white-space: nowrap;
  user-select: none;
  transition: color 0.15s, background 0.15s, border-color 0.15s;
  border: 0;
  box-sizing: border-box;
}
.tab-item--filled { border-radius: var(--r-s); }
/* Normal/White/Filled.
   ⚠️ CANONICAL-F93 (figma-component-catalog §Tab/Item "未选态填充不随模块背景"):
   the published resting fill is --bg-layer3 (#262626), which assumes a container
   noticeably darker than it. Here the filled track is --bg-layer2 (#1f1f1f), so
   #262626 would give ~1.06 contrast — segment boundaries unreadable. Stepped up
   one layer to --bg-layer4, i.e. the same step owner took in V4-2333 D19 and the
   step the catalog itself names as the likely correct binding. Still token-bound. */
.tab-item--filled.tab-item--normal { background: var(--bg-layer4); color: var(--text-2); }
/* Active/Green/Filled — the segmented-control selected variant */
.tab-item--filled.tab-item--active-green { background: var(--brand); color: var(--text-primary-btn); }
/* R9 inferred hover: "weak version of active" = brighten the label only. Raising
   the resting fill toward the active fill would read as a false selection. */
.tab-item--filled.tab-item--normal:hover { color: var(--text-body); }
.tab-item:focus-visible {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* --- Badge — mirror of canonical/Badge.vue. Figma `Badge` 4db5246d…
       Type=Rectangle × Tag=Line × Color=Green|Orange|Neutral ------------ */
.canonical-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: auto;                         /* type=Rectangle */
  height: var(--badge-circle-size);
  padding-block: var(--sp-xxs);
  padding-inline: var(--sp-s);
  border: 1px solid var(--badge-border);
  border-radius: var(--r-xl);
  box-sizing: border-box;
  background: var(--badge-bg);
  color: var(--badge-text);
  font: var(--text-style-tips);
  white-space: nowrap;
  gap: var(--sp-xxs);
}
/* fill=Line → bg transparent, border+text = palette colour */
.canonical-badge--line-green   { --badge-bg: transparent; --badge-border: var(--brand);     --badge-text: var(--brand); }
.canonical-badge--line-orange  { --badge-bg: transparent; --badge-border: var(--orange);    --badge-text: var(--orange); }
.canonical-badge--line-neutral { --badge-bg: transparent; --badge-border: var(--line-deep); --badge-text: var(--text-body); }

/* --- Tooltip — mirror of components/Tooltip/Tooltip.vue (theme=light default) */
.tooltip-wrap {
  position: relative;
  display: inline-flex;
  isolation: isolate;
}
.tooltip-trigger { display: inline-flex; }
.tooltip-box {
  /* position:absolute — overlay anchored to its trigger, no layout semantics (R14) */
  position: absolute;
  z-index: var(--z-tooltip);
  white-space: nowrap;
  max-width: var(--tooltip-max-width);
  padding: var(--sp-xxs) var(--sp-xs);
  border-radius: var(--r-s);
  background: var(--tooltip-bg);
  border: 1px solid var(--tooltip-border);
  color: var(--tooltip-text);
  font: var(--text-style-tips);
  pointer-events: none;
  box-shadow: var(--shadow-l3);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.12s;
}
.tooltip-box--light {
  --tooltip-bg: var(--tooltip-bg-light);
  --tooltip-text: var(--tooltip-text-light);
  --tooltip-border: var(--tooltip-border-light);
}
.tooltip-box--bottom { top: calc(100% + 6px); left: 50%; transform: translateX(-50%); }
.tooltip-box--bottom-start { top: calc(100% + 6px); left: 0; }
.tooltip-wrap:hover > .tooltip-box,
.tooltip-wrap:focus-within > .tooltip-box { opacity: 1; visibility: visible; }
.tooltip-arrow {
  position: absolute;
  width: 8.5px;
  height: 8.5px;
  background: var(--tooltip-bg);
  transform: rotate(45deg);
  top: -5px;
  border-top: 1px solid var(--tooltip-border);
  border-left: 1px solid var(--tooltip-border);
}
.tooltip-box--bottom > .tooltip-arrow { left: calc(50% - 4.25px); }
.tooltip-box--bottom-start > .tooltip-arrow { left: var(--sp-s); }

/* --- Chart surface — mirror of components/Chart/Chart.vue `.tvu-chart` ---
   ⚠️ SCOPED DEVIATION (declared in the delivery notes): the canonical Chart is
   an ECharts wrapper and the catalog forbids hand-rolled SVG charts. It cannot
   be imported here (single file, no CDN ⇒ no build chain, no ECharts), so per
   code-conventions R3 ("不能消费 → 手写 1:1 mirror canonical 的视觉") the plot
   below reproduces the resolved option contract from build-chart-option.ts +
   use-chart-tokens.ts. Production should render <Chart type="line" …/> instead.
   `--fluid` also relaxes the canonical fixed width/height props to a fluid
   width — a sizing-API deviation, not a visual one.
   ------------------------------------------------------------------------ */
.tvu-chart {
  position: relative;
  display: inline-block;
  box-sizing: border-box;
  padding: var(--sp-m);
  border-radius: var(--r-m);
  background: var(--bg-layer3);
}
.tvu-chart--fluid { display: block; width: 100%; }
.tvu-chart__view { display: block; }

/* axis lines / ticks / grid split lines = --line-divider-light (axisLineColor)
   tick + legend text = --text-2 (legendColor), 12px, --font-family-base */
.chart-svg { display: block; width: 100%; touch-action: pan-y; }
.chart-svg:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.chart-svg .grid-line,
.chart-svg .axis-line,
.chart-svg .axis-tick { stroke: var(--line-divider-light); stroke-width: 1; }
.chart-svg .tick-text {
  fill: var(--text-2);
  font-family: var(--font-family-base);
  font-size: var(--font-size-tips);
}
.chart-svg .label-value {
  fill: var(--text-body);
  font-family: var(--font-family-base);
  font-size: var(--font-size-tips);
  font-weight: var(--font-weight-semibold);
}
.chart-svg .series-cur { fill: none; stroke: var(--chart-color-1); stroke-width: 2; stroke-linejoin: round; }
.chart-svg .series-prev { fill: none; stroke: var(--text-tips); stroke-width: 2; stroke-dasharray: 4 4; stroke-linejoin: round; }
.chart-svg .dot-cur { fill: var(--chart-color-1); }
.chart-svg .dot-prev { fill: var(--text-tips); }
.chart-svg .crosshair { stroke: var(--text-tips); stroke-width: 1; stroke-dasharray: 3 3; }
.chart-svg .hidden { display: none; }

/* legend: bottom, icon circle, itemWidth/Height 8, 12px --text-2 */
.chart-legend {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: var(--sp-m);
  margin-top: var(--sp-xs);
}
.legend-item {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xxs);
  font: var(--text-style-tips);
  color: var(--text-2);
}
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }
.legend-dot--cur { background: var(--chart-color-1); }
.legend-dot--prev { background: var(--text-tips); }

/* Chart tooltip. The canonical Chart leaves `tooltip:{trigger:'axis'}` on the
   ECharts default (a light box with dark text and its own shadow), which is not
   token-bound. Restyled onto the DS Tooltip light variant — same visual family,
   token-bound — with wrapping allowed because an axis tooltip is multi-row. */
.chart-tip {
  /* position:absolute — floating overlay following the pointer (R14) */
  position: absolute;
  z-index: var(--z-tooltip);
  min-width: 168px;
  max-width: var(--tooltip-max-width);
  padding: var(--sp-xs);
  border-radius: var(--r-s);
  background: var(--tooltip-bg-light);
  border: 1px solid var(--tooltip-border-light);
  color: var(--tooltip-text-light);
  font: var(--text-style-tips);
  box-shadow: var(--shadow-l3);
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.1s;
}
.chart-tip.is-visible { opacity: 1; }
.chart-tip__date { font-weight: var(--font-weight-semibold); margin-bottom: var(--sp-xxs); }
.chart-tip__row { display: flex; align-items: center; gap: var(--sp-xxs); }
.chart-tip__row + .chart-tip__row { margin-top: 2px; }
.chart-tip__val { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: var(--font-weight-semibold); }
.chart-tip__delta {
  margin-top: var(--sp-xxs);
  padding-top: var(--sp-xxs);
  border-top: 1px solid var(--tooltip-border-light);
  display: flex;
  gap: var(--sp-xxs);
}

/* ==========================================================================
   4 · PAGE COMPOSITION
   ⚠️ LIBRARY GAP: the TVU library has no Card / Panel / Collapse / Accordion
   component. Verified both layers before composing: `ls src/canonical/` (39
   components) and figma-component-catalog.md headings, searched for
   card|panel|collapse|accordion|expand|divider — no match. Composed from
   surface + line + radius tokens only; candidate for tvu-design-system intake
   (BRIDGE-CODE-CANONICAL).
   ========================================================================== */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--bg-layer1);
  color: var(--text-body);
  min-height: 100vh;
}
.icon {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  fill: currentColor;
}
.icon--20 { width: 20px; height: 20px; }
.icon--24 { width: 24px; height: 24px; }
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

.page {
  max-width: var(--container-page);
  margin: 0 auto;
  padding: var(--sp-l);
  display: flex;
  flex-direction: column;
  gap: var(--sp-l);
}

/* --- page head --- */
.page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-l);
  flex-wrap: wrap;
}
.page-head__titles { display: flex; flex-direction: column; gap: var(--sp-xxs); }
.page-head__eyebrow {
  font: var(--text-style-tips);
  color: var(--text-tips);
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.page-head h1 { margin: 0; color: var(--text-heading); }
.page-head__period { font: var(--text-style-body); color: var(--text-tips); }
.page-head__controls {
  display: flex;
  align-items: center;
  gap: var(--sp-s);
  flex-wrap: wrap;
}

/* --- generic surface --- */
.card {
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-m);
  padding: var(--sp-l);
}
.card__head {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  margin-bottom: var(--sp-m);
}
.card__title { font: var(--text-style-pop-title); color: var(--text-body); }
.card__spacer { margin-left: auto; }

/* --- two-column layout --- */
.layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: var(--sp-l);
  align-items: start;
}
.layout.is-panel-collapsed { grid-template-columns: minmax(0, 1fr) 48px; }
.col-main { display: flex; flex-direction: column; gap: var(--sp-l); min-width: 0; }

/* --- overview region --- */
.kpis {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: var(--sp-l);
}
.kpi { display: flex; flex-direction: column; gap: var(--sp-xs); min-width: 0; }
.kpi__label {
  display: flex;
  align-items: center;
  gap: var(--sp-xxs);
  font: var(--text-style-tips);
  color: var(--text-tips);
}
.kpi__info { color: var(--icon-default); display: inline-flex; }
.kpi__info:hover { color: var(--icon-active); }
.kpi__valuerow { display: flex; align-items: center; gap: var(--sp-s); flex-wrap: wrap; }
/* ⚠️ GAP: the DS text-style scale tops out at large-title-24. No display /
   metric step exists, so the hero number uses the largest registered style and
   the hierarchy is carried by weight, colour and whitespace — a size is not
   invented here. */
.kpi__value {
  font: var(--text-style-large-title-24);
  color: var(--text-heading);
  font-variant-numeric: tabular-nums;
}
.kpi__value--sm {
  font: var(--text-style-headline-2);
  color: var(--text-body);
  font-variant-numeric: tabular-nums;
}
.kpi__unit { font: var(--text-style-body); color: var(--text-tips); }
.kpi__sub { font: var(--text-style-tips); color: var(--text-tips); }
/* M11 — trend visualisation: icon + the number itself carry the same colour.
   Red is deliberately NOT used for "worsening": M3 reserves red for Live. */
.trend { display: inline-flex; align-items: center; gap: var(--sp-xxs); font-variant-numeric: tabular-nums; }
.trend--improving { color: var(--brand); }
.trend--worsening { color: var(--orange); }
.trend--neutral { color: var(--text-2); }
.trend--info { color: var(--text-tips); }

/* --- chart card --- */
.chart-card__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--sp-m);
  margin-bottom: var(--sp-s);
  flex-wrap: wrap;
}
.chart-card__unit { font: var(--text-style-tips); color: var(--text-tips); }
/* Volume Context Footer — locked spec, 2026-05-20 decision (Scheme A):
   "{tokens_humanized} tokens consumed this period · ≈ ${cost_usd_humanized}
   · across {vendor_count} vendor bills" — always visible, single line, low
   contrast; hidden only when ALL values are 0. */
.volume-footer {
  margin-top: var(--sp-s);
  font: var(--text-style-tips);
  color: var(--text-tips);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- explanation panel --- */
.panel {
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-m);
  overflow: hidden;
  position: sticky;
  top: var(--sp-l);
}
.panel__head {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-m) var(--sp-m) var(--sp-s);
}
.panel__title { font: var(--text-style-pop-title); color: var(--text-body); }
.panel__body { padding: 0 var(--sp-m) var(--sp-m); }
.panel__foot {
  padding: var(--sp-s) var(--sp-m);
  border-top: 1px solid var(--line-deep);
  font: var(--text-style-tips);
  color: var(--text-tips);
}
.panel-rail {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-s);
  width: 48px;
  padding: var(--sp-s) 0;
  background: transparent;
  border: 0;
  color: var(--text-2);
  cursor: pointer;
  border-radius: var(--r-m);
  transition: background 0.15s, color 0.15s;
}
.panel-rail:hover { background: var(--bg-grey-btn-en); color: var(--text-body); }
.panel-rail:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.panel-rail__label {
  writing-mode: vertical-rl;
  font: var(--text-style-tips);
  white-space: nowrap;
}

/* accordion (composed — see LIBRARY GAP above) */
.acc { border-top: 1px solid var(--line-deep); }
.acc:first-child { border-top: 0; }
.acc__btn {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  width: 100%;
  padding: var(--sp-s) 0;
  background: transparent;
  border: 0;
  color: var(--text-body);
  font: var(--text-style-body);
  font-weight: var(--font-weight-semibold);
  text-align: left;
  cursor: pointer;
}
.acc__btn:hover { color: var(--text-heading); }
.acc__btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.acc__btn .icon { color: var(--icon-default); margin-left: auto; }
.acc__panel { padding-bottom: var(--sp-s); }
.acc__panel[hidden] { display: none; }
.acc p { margin: 0 0 var(--sp-xs); font: var(--text-style-tips); color: var(--text-2); }
.acc p:last-child { margin-bottom: 0; }
.formula {
  display: block;
  padding: var(--sp-xs);
  background: var(--bg-layer3);
  border-radius: var(--r-s);
  color: var(--text-body);
  margin-bottom: var(--sp-xs);
  overflow-x: auto;
  white-space: nowrap;
}
.dl { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-xxs) var(--sp-xs); }
.dl dt { font: var(--text-style-tips); color: var(--text-tips); }
.dl dd { margin: 0; font: var(--text-style-tips); color: var(--text-2); }

/* inputs table — R23 constrained-cell overflow */
.io-table { width: 100%; table-layout: fixed; }
.io-table th, .io-table td { padding: var(--sp-xxs) var(--sp-xxs); font-size: var(--font-size-tips); line-height: var(--line-height-tips); }
.io-table th { color: var(--text-tips); font-weight: var(--font-weight-regular); border-bottom: 1px solid var(--line-deep); }
.io-table td { border-top: 1px solid var(--line-deep); color: var(--text-2); vertical-align: top; }
.io-table col.c1 { width: 42%; }
.io-table col.c2 { width: 34%; }
.io-table col.c3 { width: 24%; }
.cell--ellipsis {
  display: block;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.io-table .tooltip-wrap { display: block; min-width: 0; }
.io-table .tooltip-trigger { display: block; min-width: 0; }

/* direction legend inside the panel (M11 three-row mapping, audience-aware
   wording — token names stay in the code, not in product UI) */
.dir-row { display: flex; align-items: center; gap: var(--sp-xs); padding: 2px 0; }
.dir-row__label { font: var(--text-style-tips); color: var(--text-2); }

/* --- responsive: literals constrained to the --bp-* value set --- */
@media (max-width: 1100px) {
  .layout { grid-template-columns: minmax(0, 1fr); }
  .layout.is-panel-collapsed { grid-template-columns: minmax(0, 1fr); }
  .panel { position: static; }
  .kpis { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 640px) {
  .page { padding: var(--sp-m); }
  .kpis { grid-template-columns: 1fr; }
  .topbar { gap: var(--sp-m); }
  .page-head { align-items: flex-start; }
}
</style>

<!-- ==========================================================================
     ICON SPRITE — every path below is copied verbatim from the TVU icon
     registry (src/icons/catalog/generated/*.ts). R1/R13: no hand-drawn SVG,
     no Unicode glyph, no emoji stands in for a product icon.
     ========================================================================== -->
<svg class="visually-hidden" aria-hidden="true" focusable="false">
  <!-- logo/tvu — the literal hex is the library asset's own; Figma SVG export
       resolves bound variables to hex (registered divergence
       `icon-variable-resolved-to-hex`). Kept verbatim. -->
  <symbol id="i-logo-tvu" viewBox="0 0 16 16">
    <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"/>
  </symbol>
  <!-- arrow/down -->
  <symbol id="i-arrow-down" viewBox="0 0 16 16">
    <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"/>
  </symbol>
  <!-- arrow/up -->
  <symbol id="i-arrow-up" viewBox="0 0 16 16">
    <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"/>
  </symbol>
  <!-- arrow/dropdown -->
  <symbol id="i-arrow-dropdown" viewBox="0 0 16 16">
    <path d="M3.05146 6.20128C3.27161 5.95374 3.65086 5.93145 3.89853 6.15148L7.86707 9.67727C7.94284 9.74458 8.05698 9.74458 8.13274 9.67727L12.1013 6.15148C12.349 5.93145 12.7282 5.95374 12.9484 6.20128C13.1685 6.44883 13.1462 6.82788 12.8985 7.04791L8.39841 11.046C8.17112 11.2479 7.8287 11.2479 7.60141 11.046L3.10129 7.04791C2.85362 6.82788 2.83131 6.44883 3.05146 6.20128Z" fill="currentColor"/>
  </symbol>
  <!-- arrow/right-2 — reused as the collapsed-disclosure chevron
       (cross-context reuse per R13 Step 3) -->
  <symbol id="i-arrow-right-2" viewBox="0 0 16 16">
    <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"/>
  </symbol>
  <!-- arrow/left-2 -->
  <symbol id="i-arrow-left-2" viewBox="0 0 16 16">
    <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"/>
  </symbol>
  <!-- message/info-1 (currentColor version; info-2 hardcodes #3892F3) -->
  <symbol id="i-info" viewBox="0 0 16 16">
    <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"/>
  </symbol>
  <!-- time/calendar -->
  <symbol id="i-calendar" viewBox="0 0 16 16">
    <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"/>
  </symbol>
</svg>

<!-- ===== TopBar (canonical mirror) ===== -->
<header class="topbar">
  <div class="topbar-brand">
    <svg class="icon icon--24" aria-hidden="true"><use href="#i-logo-tvu"/></svg>
    <span class="topbar-title" id="app-name">TVU SaaS Dashboard</span>
  </div>
  <div class="topbar-right">
    <button type="button" class="btn btn--text-gray" id="theme-toggle"></button>
  </div>
</header>

<main class="page">
  <!-- ===== page head + time-range entry points ===== -->
  <div class="page-head">
    <div class="page-head__titles">
      <span class="page-head__eyebrow" id="eyebrow"></span>
      <h1 id="page-title"></h1>
      <span class="page-head__period" id="period-line"></span>
    </div>

    <div class="page-head__controls">
      <!-- Segmented control = Tab List (Filled) + Tab/Item, per
           figma-component-catalog §Tab/Item "分段控件就用它，别用 Button".
           Roving tabindex + arrow-key navigation (the canonical TabList ships
           the roving tabindex but no arrow handler, so inactive tabs are
           keyboard-unreachable there; added here, behaviour-only). -->
      <div class="tab-list tab-list--filled">
        <div class="tab-list__items" role="tablist" aria-label="Time range" id="range-tabs"></div>
      </div>

      <button type="button" class="btn btn--ghost-gray" id="custom-range">
        <svg class="icon" aria-hidden="true"><use href="#i-calendar"/></svg>
        <span id="custom-range-label"></span>
      </button>

      <button type="button" class="btn btn--ghost-gray" id="export-btn"></button>
    </div>
  </div>

  <div class="layout" id="layout">
    <div class="col-main">
      <!-- ===== 1 · overview region ===== -->
      <section class="card" aria-labelledby="ov-title">
        <div class="card__head">
          <h2 class="card__title" id="ov-title"></h2>
        </div>

        <div class="kpis">
          <!-- hero: current unit cost + direction vs previous period -->
          <div class="kpi">
            <span class="kpi__label">
              <span id="kpi-hero-label"></span>
              <span class="tooltip-wrap">
                <span class="tooltip-trigger kpi__info" tabindex="0" aria-describedby="tip-blended">
                  <svg class="icon" aria-hidden="true"><use href="#i-info"/></svg>
                </span>
                <span class="tooltip-box tooltip-box--light tooltip-box--bottom-start" id="tip-blended" role="tooltip">
                  <span id="tip-blended-text"></span>
                  <span class="tooltip-arrow"></span>
                </span>
              </span>
            </span>
            <div class="kpi__valuerow">
              <span class="kpi__value" id="kpi-hero-value"></span>
              <span class="canonical-badge" id="kpi-hero-badge"></span>
            </div>
            <span class="kpi__sub" id="kpi-hero-sub"></span>
          </div>

          <!-- total vendor spend (exact figure; the humanised one lives in the
               chart's volume-context footer) -->
          <div class="kpi">
            <span class="kpi__label" id="kpi-spend-label"></span>
            <div class="kpi__valuerow">
              <span class="kpi__value--sm" id="kpi-spend-value"></span>
            </div>
            <span class="kpi__sub" id="kpi-spend-sub"></span>
          </div>

          <!-- data freshness / ingestion coverage (SRE persona) -->
          <div class="kpi">
            <span class="kpi__label" id="kpi-fresh-label"></span>
            <div class="kpi__valuerow">
              <span class="canonical-badge canonical-badge--line-green" id="kpi-fresh-badge"></span>
            </div>
            <span class="kpi__sub" id="kpi-fresh-sub"></span>
          </div>
        </div>
      </section>

      <!-- ===== 2 · trend chart ===== -->
      <section class="card" aria-labelledby="tr-title">
        <div class="card__head">
          <h2 class="card__title" id="tr-title"></h2>
        </div>

        <div class="chart-card__meta">
          <span class="chart-card__unit" id="chart-unit"></span>
          <span class="chart-card__unit" id="chart-compare"></span>
        </div>

        <!-- .tvu-chart surface mirrored from components/Chart/Chart.vue -->
        <figure class="tvu-chart tvu-chart--fluid" id="chart-surface" style="margin:0">
          <svg class="chart-svg" id="chart" role="img" tabindex="0"
               aria-label="Cost per 1M tokens over time, current period versus previous period"></svg>
          <div class="chart-legend">
            <span class="legend-item"><span class="legend-dot legend-dot--cur"></span><span id="legend-cur"></span></span>
            <span class="legend-item"><span class="legend-dot legend-dot--prev"></span><span id="legend-prev"></span></span>
          </div>
          <div class="chart-tip" id="chart-tip" role="status" aria-live="polite"></div>
          <figcaption class="visually-hidden">
            <table id="chart-table"></table>
          </figcaption>
        </figure>

        <!-- Volume Context Footer — locked spec (2026-05-20, Scheme A) -->
        <p class="volume-footer" id="volume-footer"></p>
        <noscript>
          <p class="volume-footer">This page renders its figures with JavaScript. Enable it to see the trend.</p>
        </noscript>
      </section>
    </div>

    <!-- ===== 3 · collapsible explanation panel ===== -->
    <aside class="panel" id="panel" aria-labelledby="panel-title">
      <div id="panel-expanded">
        <div class="panel__head">
          <svg class="icon" aria-hidden="true" style="color:var(--icon-default)"><use href="#i-info"/></svg>
          <span class="panel__title" id="panel-title"></span>
          <button type="button" class="btn btn--text-gray btn--s card__spacer" id="panel-collapse"
                  aria-expanded="true" aria-controls="panel-expanded">
            <span id="panel-collapse-label"></span>
            <svg class="icon" aria-hidden="true"><use href="#i-arrow-right-2"/></svg>
          </button>
        </div>

        <div class="panel__body">
          <!-- Formula -->
          <div class="acc">
            <button type="button" class="acc__btn" data-acc="acc-formula" aria-expanded="true" aria-controls="acc-formula">
              <span id="acc-formula-label"></span>
              <svg class="icon" aria-hidden="true"><use href="#i-arrow-dropdown"/></svg>
            </button>
            <div class="acc__panel" id="acc-formula">
              <code class="formula">cost_per_1m = vendor_cost_usd / tokens_consumed * 1,000,000</code>
              <p>Blended means spend-weighted across every vendor and model in the
                 period — not the average of the daily rates. A period where cheap
                 traffic grows faster than expensive traffic lowers the blended
                 rate even if no single vendor changed its price.</p>
              <p>The previous-period figure uses the same formula over the same
                 number of days immediately before the selected range.</p>
            </div>
          </div>

          <!-- Inputs -->
          <div class="acc">
            <button type="button" class="acc__btn" data-acc="acc-inputs" aria-expanded="true" aria-controls="acc-inputs">
              <span id="acc-inputs-label"></span>
              <svg class="icon" aria-hidden="true"><use href="#i-arrow-dropdown"/></svg>
            </button>
            <div class="acc__panel" id="acc-inputs">
              <table class="io-table">
                <colgroup><col class="c1"><col class="c2"><col class="c3"></colgroup>
                <thead>
                  <tr><th scope="col">Source</th><th scope="col">Field</th><th scope="col">Refresh</th></tr>
                </thead>
                <tbody id="io-body"></tbody>
              </table>
            </div>
          </div>

          <!-- Rules -->
          <div class="acc">
            <button type="button" class="acc__btn" data-acc="acc-rules" aria-expanded="false" aria-controls="acc-rules">
              <span id="acc-rules-label"></span>
              <svg class="icon" aria-hidden="true"><use href="#i-arrow-right-2"/></svg>
            </button>
            <div class="acc__panel" id="acc-rules" hidden>
              <dl class="dl">
                <dt>Included</dt>
                <dd>Every metered vendor line item billed inside the period.</dd>
                <dt>Excluded</dt>
                <dd>One-off platform fees. Credits and refunds land in the period
                    they are issued, so a refunded month can read lower than it billed.</dd>
                <dt>Currency</dt>
                <dd>All bills are converted to USD at the period-close rate. The
                    <code>≈</code> prefix marks a converted equivalent, not a billed amount.</dd>
                <dt>Tokens</dt>
                <dd>Prompt and completion tokens combined, as reported by the usage
                    pipeline. Cached-prompt reads count once.</dd>
              </dl>
            </div>
          </div>

          <!-- How to read the direction (M11 three-layer signal, in plain words) -->
          <div class="acc">
            <button type="button" class="acc__btn" data-acc="acc-dir" aria-expanded="true" aria-controls="acc-dir">
              <span id="acc-dir-label"></span>
              <svg class="icon" aria-hidden="true"><use href="#i-arrow-dropdown"/></svg>
            </button>
            <div class="acc__panel" id="acc-dir">
              <div class="dir-row">
                <span class="trend trend--improving">
                  <svg class="icon" aria-hidden="true"><use href="#i-arrow-down"/></svg>8.6%
                </span>
                <span class="dir-row__label">Improving — unit cost fell by 1.0% or more.</span>
              </div>
              <div class="dir-row">
                <span class="trend trend--worsening">
                  <svg class="icon" aria-hidden="true"><use href="#i-arrow-up"/></svg>2.6%
                </span>
                <span class="dir-row__label">Worsening — unit cost rose by 1.0% or more.</span>
              </div>
              <div class="dir-row">
                <span class="trend trend--neutral">0.4%</span>
                <span class="dir-row__label">Stable — change within ±1.0%, shown without an arrow.</span>
              </div>
              <p style="margin-top:var(--sp-xs)">Spend and token volume show their
                 direction in neutral grey: growing volume is neither good nor bad
                 on its own, so it is never scored.</p>
            </div>
          </div>
        </div>

        <p class="panel__foot" id="panel-foot"></p>
      </div>

      <!-- collapsed rail: a real button with a real text label (M9), not an
           icon-only affordance -->
      <button type="button" class="panel-rail" id="panel-expand" hidden aria-expanded="false" aria-controls="panel-expanded">
        <svg class="icon" aria-hidden="true"><use href="#i-arrow-left-2"/></svg>
        <span class="panel-rail__label" id="panel-expand-label"></span>
      </button>
    </aside>
  </div>
</main>

<script>
/* ==========================================================================
   R16 — product UI strings live in one const map (an i18n seam even before an
   i18n framework exists). No bilingual literals in product UI.
   ========================================================================== */
const T = {
  appName: 'TVU SaaS Dashboard',
  eyebrow: 'Usage & cost',
  pageTitle: 'Cost per Token Trend',
  themeLight: 'Light theme',
  themeDark: 'Dark theme',
  customRange: 'Custom range',
  exportCsv: 'Export CSV',
  overview: 'Current period',
  trend: 'Unit cost over time',
  heroLabel: 'Cost per 1M tokens (blended)',
  heroTip: 'Blended = total vendor spend ÷ tokens consumed',
  spendLabel: 'Total vendor spend',
  freshLabel: 'Data freshness',
  freshBadge: 'Synced',
  chartUnit: 'USD per 1M tokens',
  legendCur: 'Current period',
  legendPrev: 'Previous period',
  panelTitle: 'How this is calculated',
  collapse: 'Collapse',
  expandRail: 'How this is calculated',
  accFormula: 'Formula',
  accInputs: 'Inputs',
  accRules: 'What counts',
  accDirection: 'Reading the direction',
  tableCaption: 'Cost per 1M tokens by period',
  colPeriod: 'Period',
  improving: 'Improving',
  worsening: 'Worsening',
  stable: 'Stable',
  // Volume Context Footer — locked format, 2026-05-20 decision (Scheme A)
  volumeFooter: (tokens, cost, vendors) =>
    `${tokens} tokens consumed this period · ≈ ${cost} · across ${vendors} vendor bills`,
  vsPrevious: (period, value) => `Previous ${period}: ${value}`,
  vsPreviousShort: (period) => `vs previous ${period}`,
  ingested: (n) => `${n} of ${n} vendor bills ingested`,
  syncedAgo: 'Last synced 12 min ago',
  updatedAt: 'Figures updated Aug 25, 2026 · 14:20 UTC',
  comparedWith: (label) => `Compared with ${label}`,
  rangeSummary: (a, b) => `${a}  ·  previous ${b}`
};

/* Inputs table — one row is deliberately long enough to truncate, so the R23
   ellipsis + truncation-detection + tooltip path is actually exercised. */
const INPUTS = [
  { source: 'Vendor billing API — consolidated invoice feed', field: 'vendor_cost_usd', refresh: 'Daily 02:00 UTC' },
  { source: 'Usage pipeline', field: 'tokens_consumed', refresh: 'Every 5 min' },
  { source: 'Treasury FX snapshot (period close)', field: 'fx_rate_usd', refresh: 'Daily' }
];

/* ==========================================================================
   DATA
   Internally consistent by construction: the 7-day series is literally the
   tail of the 30-day series, and its comparison series is the 7 days before
   that. Blended figures are spend-weighted (total spend ÷ total tokens), which
   is why they sit slightly below the arithmetic mean of the daily points.
   ========================================================================== */
const D30 = [1.216,1.209,1.198,1.204,1.187,1.176,1.168,1.159,1.171,1.148,
             1.136,1.129,1.117,1.108,1.121,1.096,1.084,1.072,1.061,1.055,
             1.048,1.039,1.044,1.058,1.071,1.066,1.083,1.094,1.107,1.112];
const P30 = [1.196,1.202,1.199,1.208,1.212,1.205,1.216,1.221,1.214,1.223,
             1.219,1.228,1.214,1.206,1.212,1.219,1.207,1.213,1.204,1.211,
             1.218,1.209,1.201,1.214,1.206,1.198,1.209,1.216,1.203,1.211];

function labelsAug(from, count) {
  // Jul 27 → Aug 25, 2026
  const out = [];
  for (let i = 0; i < count; i++) {
    const d = from + i;
    out.push(d <= 31 ? `Jul ${d}` : `Aug ${d - 31}`);
  }
  return out;
}

const RANGES = {
  '7d': {
    tab: '7 days',
    periodWord: '7 days',
    period: 'Aug 19 – Aug 25, 2026',
    prevPeriod: 'Aug 12 – Aug 18, 2026',
    labels: ['Aug 19','Aug 20','Aug 21','Aug 22','Aug 23','Aug 24','Aug 25'],
    cur: D30.slice(23),
    prev: D30.slice(16, 23),
    unit: 1.0847, prevUnit: 1.0576,
    spend: 1280, prevSpend: 1185,
    tokensH: '1.18B', prevTokensH: '1.12B', tokensPct: 5.4,
    costH: '$1.3K', vendors: 12,
    labelEvery: 1
  },
  '30d': {
    tab: '30 days',
    periodWord: '30 days',
    period: 'Jul 27 – Aug 25, 2026',
    prevPeriod: 'Jun 27 – Jul 26, 2026',
    labels: labelsAug(27, 30),
    cur: D30,
    prev: P30,
    unit: 1.1064, prevUnit: 1.2107,
    spend: 5200, prevSpend: 5000,
    tokensH: '4.7B', prevTokensH: '4.13B', tokensPct: 13.8,
    costH: '$5.2K', vendors: 14,
    labelEvery: 5
  },
  '90d': {
    tab: '90 days',
    periodWord: '90 days',
    period: 'May 28 – Aug 25, 2026',
    prevPeriod: 'Feb 27 – May 27, 2026',
    labels: ['Jun 2','Jun 9','Jun 16','Jun 23','Jun 30','Jul 7','Jul 14','Jul 21','Jul 28','Aug 4','Aug 11','Aug 18','Aug 25'],
    cur:  [1.436,1.412,1.398,1.371,1.352,1.331,1.298,1.264,1.208,1.181,1.113,1.058,1.084],
    prev: [1.512,1.489,1.478,1.462,1.455,1.441,1.468,1.433,1.427,1.439,1.421,1.446,1.430],
    unit: 1.2437, prevUnit: 1.4483,
    spend: 14800, prevSpend: 12600,
    tokensH: '11.9B', prevTokensH: '8.7B', tokensPct: 36.8,
    costH: '$14.8K', vendors: 16,
    labelEvery: 2
  },
  '12m': {
    tab: '12 months',
    periodWord: '12 months',
    period: 'Sep 2025 – Aug 2026',
    prevPeriod: 'Sep 2024 – Aug 2025',
    labels: ['Sep','Oct','Nov','Dec','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug'],
    cur:  [1.71,1.68,1.64,1.62,1.58,1.55,1.51,1.47,1.42,1.34,1.21,1.09],
    prev: [1.94,1.91,1.96,1.89,1.93,1.87,1.90,1.85,1.88,1.84,1.86,1.82],
    unit: 1.3722, prevUnit: 1.8804,
    spend: 54200, prevSpend: 34600,
    tokensH: '39.5B', prevTokensH: '18.4B', tokensPct: 114.7,
    costH: '$54.2K', vendors: 21,
    labelEvery: 1
  }
};

const STABLE_THRESHOLD = 1.0; // percent — documented in the panel
const state = { range: '30d', cursor: null };

/* ---------- formatting ---------- */
const usd2 = (v) => '$' + v.toFixed(2);
const usdInt = (v) => '$' + v.toLocaleString('en-US');
const pct1 = (v) => Math.abs(v).toFixed(1) + '%';
const el = (id) => document.getElementById(id);

function iconSvg(id, extraClass) {
  return `<svg class="icon${extraClass ? ' ' + extraClass : ''}" aria-hidden="true"><use href="#${id}"/></svg>`;
}

/* M11 — direction is a two-layer signal: the arrow AND the number carry the
   same colour. Metrics without a quality direction (spend, volume) get the
   arrow for direction only, in neutral ink. */
function trendClass(deltaPct) {
  if (Math.abs(deltaPct) < STABLE_THRESHOLD) return 'trend--neutral';
  return deltaPct < 0 ? 'trend--improving' : 'trend--worsening';
}
function trendWord(deltaPct) {
  if (Math.abs(deltaPct) < STABLE_THRESHOLD) return T.stable;
  return deltaPct < 0 ? T.improving : T.worsening;
}
function trendIcon(deltaPct) {
  if (Math.abs(deltaPct) < STABLE_THRESHOLD) return '';
  return iconSvg(deltaPct < 0 ? 'i-arrow-down' : 'i-arrow-up');
}

/* ==========================================================================
   RANGE TABS — Tab List (Filled) + Tab/Item, roving tabindex + arrow keys
   ========================================================================== */
function buildTabs() {
  const wrap = el('range-tabs');
  wrap.innerHTML = Object.keys(RANGES).map((key) => {
    const active = key === state.range;
    return `<div class="tab-item tab-item--filled ${active ? 'tab-item--active-green' : 'tab-item--normal'}"
                 role="tab" data-range="${key}" aria-selected="${active}"
                 tabindex="${active ? 0 : -1}">${RANGES[key].tab}</div>`;
  }).join('');
}
el('range-tabs').addEventListener('click', (e) => {
  const item = e.target.closest('[data-range]');
  if (item) selectRange(item.dataset.range);
});
el('range-tabs').addEventListener('keydown', (e) => {
  const keys = Object.keys(RANGES);
  const i = keys.indexOf(state.range);
  let next = null;
  if (e.key === 'ArrowRight') next = keys[Math.min(i + 1, keys.length - 1)];
  else if (e.key === 'ArrowLeft') next = keys[Math.max(i - 1, 0)];
  else if (e.key === 'Home') next = keys[0];
  else if (e.key === 'End') next = keys[keys.length - 1];
  else if (e.key === 'Enter' || e.key === ' ') {
    const item = e.target.closest('[data-range]');
    if (item) { e.preventDefault(); selectRange(item.dataset.range); }
    return;
  }
  if (next) {
    e.preventDefault();
    selectRange(next);
    el('range-tabs').querySelector(`[data-range="${next}"]`).focus();
  }
});
function selectRange(key) {
  if (!RANGES[key] || key === state.range) return;
  state.range = key;
  state.cursor = null;
  buildTabs();
  renderAll();
}

/* ==========================================================================
   OVERVIEW + FOOTER
   ========================================================================== */
function renderStatic() {
  el('app-name').textContent = T.appName;
  el('eyebrow').textContent = T.eyebrow;
  el('page-title').textContent = T.pageTitle;
  el('ov-title').textContent = T.overview;
  el('tr-title').textContent = T.trend;
  el('chart-unit').textContent = T.chartUnit;
  el('kpi-hero-label').textContent = T.heroLabel;
  el('tip-blended-text').textContent = T.heroTip;
  el('kpi-spend-label').textContent = T.spendLabel;
  el('kpi-fresh-label').textContent = T.freshLabel;
  el('legend-cur').textContent = T.legendCur;
  el('legend-prev').textContent = T.legendPrev;
  el('panel-title').textContent = T.panelTitle;
  el('panel-collapse-label').textContent = T.collapse;
  el('panel-expand-label').textContent = T.expandRail;
  el('acc-formula-label').textContent = T.accFormula;
  el('acc-inputs-label').textContent = T.accInputs;
  el('acc-rules-label').textContent = T.accRules;
  el('acc-dir-label').textContent = T.accDirection;
  el('panel-foot').textContent = T.updatedAt;
  el('custom-range-label').textContent = T.customRange;
  el('export-btn').textContent = T.exportCsv;
  syncThemeLabel();
}

function renderOverview() {
  const r = RANGES[state.range];
  const dUnit = (r.unit / r.prevUnit - 1) * 100;
  const dSpend = (r.spend / r.prevSpend - 1) * 100;

  el('period-line').textContent = T.rangeSummary(r.period, r.prevPeriod);
  el('chart-compare').textContent = T.comparedWith(r.prevPeriod);

  el('kpi-hero-value').textContent = usd2(r.unit);

  // delta chip = Badge (Rectangle × Line × Green|Orange|Neutral)
  const badge = el('kpi-hero-badge');
  const cls = trendClass(dUnit);
  badge.className = 'canonical-badge ' +
    (cls === 'trend--improving' ? 'canonical-badge--line-green'
      : cls === 'trend--worsening' ? 'canonical-badge--line-orange'
      : 'canonical-badge--line-neutral');
  badge.innerHTML = `${trendIcon(dUnit)}<span>${pct1(dUnit)} ${T.vsPreviousShort(r.periodWord)}</span>`;
  badge.setAttribute('aria-label',
    `${trendWord(dUnit)}: ${pct1(dUnit)} ${T.vsPreviousShort(r.periodWord)}`);

  el('kpi-hero-sub').textContent =
    `${trendWord(dUnit)} · ${T.vsPrevious(r.periodWord, usd2(r.prevUnit))}`;

  el('kpi-spend-value').textContent = usdInt(r.spend);
  el('kpi-spend-sub').innerHTML =
    `<span class="trend trend--info">${iconSvg(dSpend < 0 ? 'i-arrow-down' : 'i-arrow-up')}${pct1(dSpend)}</span>` +
    ` ${T.vsPreviousShort(r.periodWord)} · ${r.tokensH} tokens (${r.prevTokensH} before)`;

  el('kpi-fresh-badge').textContent = T.freshBadge;
  el('kpi-fresh-sub').textContent = `${T.syncedAgo} · ${T.ingested(r.vendors)}`;

  // Volume Context Footer — always visible; hidden only when ALL values are 0
  const footer = el('volume-footer');
  const hasVolume = !(r.tokensH === '0' && r.spend === 0 && r.vendors === 0);
  footer.hidden = !hasVolume;
  if (hasVolume) footer.textContent = T.volumeFooter(r.tokensH, r.costH, r.vendors);
}

/* ==========================================================================
   CHART — hand-authored mirror of <Chart type="line">
   ========================================================================== */
const M = { top: 18, right: 68, bottom: 34, left: 56 };
const CHART_H = 300;

function niceStep(raw) {
  const mag = Math.pow(10, Math.floor(Math.log10(raw)));
  const norm = raw / mag;
  const mult = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 2.5 ? 2.5 : norm <= 5 ? 5 : 10;
  return mult * mag;
}
function niceDomain(min, max) {
  const span = (max - min) || 1;
  const pad = span * 0.15;
  const step = niceStep((max + pad - (min - pad)) / 4);
  return { lo: Math.floor((min - pad) / step) * step, hi: Math.ceil((max + pad) / step) * step, step };
}

let geom = null;

function renderChart() {
  const r = RANGES[state.range];
  const svg = el('chart');
  const width = Math.max(320, Math.round(el('chart-surface').clientWidth - 32)); // minus --sp-m padding ×2
  svg.setAttribute('width', width);
  svg.setAttribute('height', CHART_H);

  const all = r.cur.concat(r.prev);
  const { lo, hi, step } = niceDomain(Math.min(...all), Math.max(...all));
  const plotW = width - M.left - M.right;
  const plotH = CHART_H - M.top - M.bottom;
  const n = r.cur.length;
  const x = (i) => M.left + (n === 1 ? plotW / 2 : (plotW * i) / (n - 1));
  const y = (v) => M.top + plotH - ((v - lo) / (hi - lo)) * plotH;
  geom = { x, y, plotW, plotH, n, width };

  let out = '';

  // value-axis split lines + labels (splitLine / axisLabel)
  for (let v = lo; v <= hi + 1e-9; v += step) {
    const yy = y(v).toFixed(1);
    out += `<line class="grid-line" x1="${M.left}" y1="${yy}" x2="${M.left + plotW}" y2="${yy}"/>`;
    out += `<text class="tick-text" x="${M.left - 8}" y="${(y(v) + 4).toFixed(1)}" text-anchor="end">$${v.toFixed(2)}</text>`;
  }

  // category axis line + ticks + labels
  const base = M.top + plotH;
  out += `<line class="axis-line" x1="${M.left}" y1="${base}" x2="${M.left + plotW}" y2="${base}"/>`;
  r.labels.forEach((lab, i) => {
    const show = i % r.labelEvery === 0 || i === n - 1;
    if (!show) return;
    const xx = x(i);
    out += `<line class="axis-tick" x1="${xx.toFixed(1)}" y1="${base}" x2="${xx.toFixed(1)}" y2="${base + 4}"/>`;
    const anchor = i === 0 ? 'start' : i === n - 1 ? 'end' : 'middle';
    out += `<text class="tick-text" x="${xx.toFixed(1)}" y="${base + 20}" text-anchor="${anchor}">${lab}</text>`;
  });

  const path = (arr) => arr.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(' ');
  out += `<path class="series-prev" d="${path(r.prev)}"/>`;
  out += `<path class="series-cur" d="${path(r.cur)}"/>`;

  // markers only when the point count stays legible (mirrors the component's
  // symbol behaviour); otherwise just the end point
  if (n <= 13) {
    r.prev.forEach((v, i) => { out += `<circle class="dot-prev" cx="${x(i).toFixed(1)}" cy="${y(v).toFixed(1)}" r="3"/>`; });
    r.cur.forEach((v, i) => { out += `<circle class="dot-cur" cx="${x(i).toFixed(1)}" cy="${y(v).toFixed(1)}" r="3.5"/>`; });
  } else {
    out += `<circle class="dot-cur" cx="${x(n - 1).toFixed(1)}" cy="${y(r.cur[n - 1]).toFixed(1)}" r="3.5"/>`;
  }

  // one selective direct label on the last current point; the label wears a
  // text token, the adjacent coloured marker carries series identity
  out += `<text class="label-value" x="${(x(n - 1) + 10).toFixed(1)}" y="${(y(r.cur[n - 1]) + 4).toFixed(1)}">${usd2(r.cur[n - 1])}</text>`;

  // crosshair (axis pointer) + hover dots
  out += `<g id="ch-cross" class="hidden">
            <line class="crosshair" x1="0" y1="${M.top}" x2="0" y2="${base}"/>
            <circle class="dot-prev" r="4" cx="0" cy="0"/>
            <circle class="dot-cur" r="4.5" cx="0" cy="0"/>
          </g>`;

  svg.innerHTML = out;
  renderTable();
}

/* accessible alternative to the plot (identity never colour-alone) */
function renderTable() {
  const r = RANGES[state.range];
  let rows = `<caption>${T.tableCaption} — ${r.period}</caption>
              <tr><th scope="col">${T.colPeriod}</th><th scope="col">${T.legendCur}</th><th scope="col">${T.legendPrev}</th></tr>`;
  r.labels.forEach((lab, i) => {
    rows += `<tr><th scope="row">${lab}</th><td>${usd2(r.cur[i])}</td><td>${usd2(r.prev[i])}</td></tr>`;
  });
  el('chart-table').innerHTML = rows;
}

/* ---------- hover / keyboard read-out ---------- */
function showCursor(i) {
  const r = RANGES[state.range];
  if (!geom || i == null || i < 0 || i >= geom.n) return hideCursor();
  state.cursor = i;
  const g = document.getElementById('ch-cross');
  const [line, dPrev, dCur] = g.children;
  const xx = geom.x(i);
  line.setAttribute('x1', xx); line.setAttribute('x2', xx);
  dPrev.setAttribute('cx', xx); dPrev.setAttribute('cy', geom.y(r.prev[i]));
  dCur.setAttribute('cx', xx); dCur.setAttribute('cy', geom.y(r.cur[i]));
  g.classList.remove('hidden');

  const d = (r.cur[i] / r.prev[i] - 1) * 100;
  const tip = el('chart-tip');
  tip.innerHTML =
    `<div class="chart-tip__date">${r.labels[i]}</div>
     <div class="chart-tip__row"><span class="legend-dot legend-dot--cur"></span>${T.legendCur}
       <span class="chart-tip__val">${usd2(r.cur[i])}</span></div>
     <div class="chart-tip__row"><span class="legend-dot legend-dot--prev"></span>${T.legendPrev}
       <span class="chart-tip__val">${usd2(r.prev[i])}</span></div>
     <div class="chart-tip__delta"><span class="trend ${trendClass(d)}">${trendIcon(d)}${pct1(d)}</span>
       <span>${trendWord(d)}</span></div>`;
  tip.classList.add('is-visible');

  const tw = tip.offsetWidth || 180;
  let left = xx + 16 + 16; // + .tvu-chart padding
  if (left + tw > geom.width) left = xx + 16 - tw - 16;
  tip.style.left = Math.max(8, left) + 'px';
  tip.style.top = (M.top + 16) + 'px';
}
function hideCursor() {
  state.cursor = null;
  const g = document.getElementById('ch-cross');
  if (g) g.classList.add('hidden');
  el('chart-tip').classList.remove('is-visible');
}
function indexFromX(clientX) {
  const box = el('chart').getBoundingClientRect();
  const px = clientX - box.left;
  const t = (px - M.left) / (geom.plotW || 1);
  return Math.max(0, Math.min(geom.n - 1, Math.round(t * (geom.n - 1))));
}
el('chart').addEventListener('pointermove', (e) => showCursor(indexFromX(e.clientX)));
el('chart').addEventListener('pointerleave', hideCursor);
el('chart').addEventListener('keydown', (e) => {
  if (!geom) return;
  const cur = state.cursor == null ? geom.n - 1 : state.cursor;
  if (e.key === 'ArrowRight') { e.preventDefault(); showCursor(Math.min(cur + 1, geom.n - 1)); }
  else if (e.key === 'ArrowLeft') { e.preventDefault(); showCursor(Math.max(cur - 1, 0)); }
  else if (e.key === 'Home') { e.preventDefault(); showCursor(0); }
  else if (e.key === 'End') { e.preventDefault(); showCursor(geom.n - 1); }
  else if (e.key === 'Escape') hideCursor();
});
el('chart').addEventListener('blur', hideCursor);

/* ==========================================================================
   R23 — constrained-cell overflow: ellipsis + truncation detection, and a
   tooltip ONLY where the text is actually clipped (plus `title` as the no-JS /
   touch fallback).
   ========================================================================== */
function renderInputs() {
  el('io-body').innerHTML = INPUTS.map((row, i) => `
    <tr>
      <td>
        <span class="tooltip-wrap" data-truncate="${i}">
          <span class="tooltip-trigger">
            <span class="cell--ellipsis" title="${row.source}">${row.source}</span>
          </span>
        </span>
      </td>
      <td><code>${row.field}</code></td>
      <td>${row.refresh}</td>
    </tr>`).join('');
  requestAnimationFrame(applyTruncationTooltips);
}
function applyTruncationTooltips() {
  document.querySelectorAll('.io-table .cell--ellipsis').forEach((cell, i) => {
    const wrap = cell.closest('.tooltip-wrap');
    wrap.querySelector('.tooltip-box')?.remove();
    const truncated = cell.scrollWidth > cell.clientWidth;
    if (!truncated) { cell.removeAttribute('aria-describedby'); return; }
    const id = 'tip-io-' + i;
    cell.setAttribute('aria-describedby', id);
    const box = document.createElement('span');
    box.className = 'tooltip-box tooltip-box--light tooltip-box--bottom-start';
    box.id = id;
    box.setAttribute('role', 'tooltip');
    box.textContent = cell.textContent;
    box.insertAdjacentHTML('beforeend', '<span class="tooltip-arrow"></span>');
    wrap.appendChild(box);
  });
}

/* ==========================================================================
   DISCLOSURE — panel + accordion. The chevron swaps between two library icons
   rather than rotating a glyph, so the icon itself reports the state.
   ========================================================================== */
function setChevron(btn, expanded) {
  const use = btn.querySelector('use');
  if (use) use.setAttribute('href', expanded ? '#i-arrow-dropdown' : '#i-arrow-right-2');
}
document.querySelectorAll('[data-acc]').forEach((btn) => {
  btn.addEventListener('click', () => {
    const panel = document.getElementById(btn.dataset.acc);
    const open = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', String(!open));
    panel.hidden = open;
    setChevron(btn, !open);
    if (!open) requestAnimationFrame(applyTruncationTooltips);
  });
});
el('panel-collapse').addEventListener('click', () => setPanel(false));
el('panel-expand').addEventListener('click', () => setPanel(true));
function setPanel(open) {
  el('layout').classList.toggle('is-panel-collapsed', !open);
  el('panel-expanded').hidden = !open;
  el('panel-expand').hidden = open;
  el('panel-collapse').setAttribute('aria-expanded', String(open));
  el('panel-expand').setAttribute('aria-expanded', String(open));
  if (open) {
    el('panel-collapse').focus();
    requestAnimationFrame(() => { renderChart(); applyTruncationTooltips(); });
  } else {
    el('panel-expand').focus();
    requestAnimationFrame(renderChart);
  }
}

/* ---------- theme (dark is the design-system default) ---------- */
function syncThemeLabel() {
  const light = document.documentElement.getAttribute('data-theme') === 'light';
  el('theme-toggle').textContent = light ? T.themeDark : T.themeLight;
}
el('theme-toggle').addEventListener('click', () => {
  const light = document.documentElement.getAttribute('data-theme') === 'light';
  if (light) document.documentElement.removeAttribute('data-theme');
  else document.documentElement.setAttribute('data-theme', 'light');
  syncThemeLabel();
  hideCursor();
});

/* ---------- boot ---------- */
function renderAll() {
  renderOverview();
  renderChart();
}
renderStatic();
buildTabs();
renderInputs();
renderAll();
new ResizeObserver(() => { renderChart(); applyTruncationTooltips(); }).observe(el('chart-surface'));
</script>
```

打开后可验的四条：切区间时 **7 days** 会翻成 ↑ 橙（恶化），其余三档是 ↓ 绿（改善）——同一套 M11 规则两个方向都跑到了；图表 hover / 键盘 `←→` 都出十字线 + tooltip；右侧面板可整体折成 48px rail，内部四段各自可折；`Inputs` 表第一行来源足够长，会真触发截断检测并挂上 tooltip（第二、三行不会，短文本不该弹）。

**收尾没做**（本 session 只读、Bash 停用）：没写 work-log、没更新 STATUS、没落 handoff。这三件按纪律该做，等你放开写权限或换 session 我补；相应地我也**没有**给续跑 prompt——收尾动作没做完就甩 prompt 等于把收尾丢回给你。