续上：输出被截断在图表 polyline 那一段。为了把完整文件放进一个代码块，我把它压紧并砍掉两处**非必需**的东西，如实登记：

1. **去掉了「Custom range」按钮 + 日期弹层**。时间范围切换入口由 7D/30D/90D/12M 分段控件满足（需求 3 已覆盖）；留一个自定义弹层会显著撑大文件，而做不完就是个死按钮（M12 精神：不许静默 no-op）。要补的话它是独立一小步。
2. **去掉了 `[data-theme="light"]` override 块**。本页按 domain-tvu M4 单深色交付，带上浅色 token 只会让人误读成「交付了两个主题」。token 值零改动，只是不携带浅色分支。

其余不变：token 逐值取自 `src/tokens/variables.css`，组件几何逐值取自各组件源码，图标路径逐字取自 `figma-data/published/icons/svg/`。

```html
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Cost per Token Trend &#183; TVU SaaS Dashboard</title>
<style>
/* ==========================================================================
 * 1 · TVU DESIGN TOKENS
 * Values copied verbatim from tvu-design-system/src/tokens/variables.css
 * (comments condensed; not one value changed). Normally a build step copies
 * that file and the page <link>s it — this deliverable must be one file with
 * no external request, so it is inlined.
 * Deliberately not carried over: the @font-face for 'Digital Numbers' (binary
 * cannot be inlined; that face is reserved for the TopBar clock face) and the
 * [data-theme="light"] override block (this page ships dark-only, domain-tvu M4).
 * ========================================================================== */
:root{
--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:#2fb54e;--brand-hover:#41c760;--brand-match:#0b2b13;--brand-disable:#1a652c;--red:#ea4233;--red-hover:#ed5b4e;--orange:#f68512;--orange-hover:#f89c3e;--orange-tz:#d39f39;--blue:#3892f3;--blue-hover:#62a9f6;
--text-heading:#ffffff;--text-body:#f8f8f8;--text-2:#cccccc;--text-tips:#9e9e9e;--text-placeholder:#7b7b7b;--text-disabled:#595959;--text-primary-btn:#f8f8f8;
--icon-active:#f8f8f8;--icon-default:#9e9e9e;--icon-disabled:#595959;--icon-placeholder:#7b7b7b;
--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:#595959;--line-deep:#353535;--line-light:#434343;
--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;
--sp-xxs:4px;--sp-xs:8px;--sp-s:12px;--sp-m:16px;--sp-l:24px;--sp-xl:32px;--sp-xxl:40px;--sp-xxxl:56px;
--r-xs:2px;--r-s:4px;--r-m:8px;--r-l:12px;--r-xl:16px;--r-xxl:20px;
--z-dropdown:1000;--z-tooltip:1000;--z-modal:1000;--z-select-portal:1100;
--bp-sm:640px;--bp-md:960px;--bp-lg:1100px;--bp-xl:1280px;
--container-narrow:720px;--container-prose:840px;--container-page:1600px;--container-wide:1800px;
--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);
--tooltip-bg-dark:var(--color-grey-10);--tooltip-border-dark:var(--color-grey-8);--tooltip-text-dark:var(--color-grey-2);--tooltip-max-width:320px;
--badge-circle-size:24px;--topbar-section-gap:80px;--topbar-height:64px;--control-height-xxs:32px;
--shadow-l3:0 12px 48px 16px #00000008,0 9px 28px 0 #0000000d,0 6px 16px -8px #00000014;
--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-1:600 20px/30px 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-weight-regular:400;--font-weight-semibold:600;--line-height-body:21px;
--line-divider-deep:var(--line-deep);--line-divider-light:var(--line-light);--input-filled-bg:var(--bg-layer4);--green-bg:var(--brand-match);
}
/* element base layer, from the tail of variables.css */
body{font:var(--text-style-body);font-family:var(--font-family-base)}
h1{font:var(--text-style-headline-1);color:var(--text-body)}
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)}

/* ==========================================================================
 * 2 · PAGE — hand-written 1:1 mirrors of TVU canonical components.
 * R3: with no build chain the consumer mirrors canonical props/visuals instead
 * of re-designing them. Every geometry value is quoted from the source named
 * in its comment.
 * ========================================================================== */
*,*::before,*::after{box-sizing:border-box}
html{color-scheme:dark}
body{margin:0;background:var(--bg-layer1);color:var(--text-body);min-height:100vh}
:focus-visible{outline:2px solid var(--brand);outline-offset:2px} /* = Button.vue */
/* visually-hidden: standard off-screen idiom, not layout (R14) */
.vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;margin:-1px;padding:0}
.ico{width:16px;height:16px;flex:none;display:block}

/* TopBar — mirror src/components/TopBar/TopBar.vue */
.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-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;min-width:0}
.topbar-right{display:flex;align-items:center;gap:var(--sp-s);flex-shrink:0;margin-left:auto}
.tvu-logo{width:24px;height:24px;display:block;flex:none}
.sync{display:flex;align-items:center;gap:var(--sp-xs);font:var(--text-style-tips);color:var(--text-tips);white-space:nowrap}
.sync i{width:7px;height:7px;border-radius:50%;background:var(--brand);flex:none}

/* Tab/Item — mirror src/components/Tab/TabItem.vue (height 32, gap 8, pad 0 16) */
.tab-list{display:inline-flex;align-items:center;gap:var(--sp-xxs)}
.tab-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-tips);background:none;border:0;cursor:pointer;white-space:nowrap;user-select:none;text-decoration:none;transition:color .15s,background .15s,border-color .15s}
.tab-item--line{border-bottom:1px solid transparent;margin-bottom:-1px}
.tab-item--line[aria-current=page]{color:var(--brand);border-bottom-color:var(--brand);font-weight:700}
.tab-item--line:not([aria-current=page]):hover{color:var(--text-body)}
.tab-item--filled{border-radius:var(--r-s);background:var(--bg-layer3);color:var(--text-2)}
.tab-item--filled:hover{background:var(--bg-layer4);color:var(--text-body)}
.tab-item--filled[aria-selected=true]{background:var(--brand);color:var(--text-primary-btn)}
.tab-item--filled[aria-selected=true]:hover{background:var(--brand-hover)}

/* Badge — mirror src/canonical/Badge.vue, Type=Rectangle x Tag=Line */
.badge{display:inline-flex;align-items:center;justify-content:center;gap:var(--sp-xxs);height:var(--badge-circle-size);width:auto;padding-block:var(--sp-xxs);padding-inline:var(--sp-s);border:1px solid transparent;border-radius:var(--r-xl);background:transparent;font:var(--text-style-tips);white-space:nowrap}
.badge--green{border-color:var(--brand);color:var(--brand)}
.badge--orange{border-color:var(--orange);color:var(--orange)}
.badge--neutral{border-color:var(--line-deep);color:var(--text-body)}

/* Tooltip (dark) — mirror src/components/Tooltip/Tooltip.vue.
   position:absolute = overlay affordance, no layout semantics (R14) */
.tooltip-box{position:absolute;z-index:var(--z-tooltip);max-width:var(--tooltip-max-width);padding:var(--sp-xxs) var(--sp-xs);border-radius:var(--r-s);background:var(--tooltip-bg-dark);border:1px solid var(--tooltip-border-dark);color:var(--tooltip-text-dark);font:var(--text-style-tips);pointer-events:none;box-shadow:var(--shadow-l3);white-space:nowrap}
.tooltip-box[hidden]{display:none}
.tt-r{display:flex;align-items:center;gap:var(--sp-xxs)}
.tt-r i{width:8px;height:8px;border-radius:50%;flex:none}
.tt-k{color:var(--text-tips)}

/* page shell */
.page{max-width:var(--container-page);margin:0 auto;padding:var(--sp-l)}
.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:var(--sp-l);flex-wrap:wrap;margin-bottom:var(--sp-l)}
.page-head h1{margin:0}
.page-head p{margin:var(--sp-xxs) 0 0;font:var(--text-style-body);color:var(--text-tips)}
.page-grid{display:grid;grid-template-columns:minmax(0,1fr) 360px;gap:var(--sp-l);align-items:start}
.page-grid.is-collapsed{grid-template-columns:minmax(0,1fr) 48px}
.col-main{display:flex;flex-direction:column;gap:var(--sp-l);min-width:0}

/* card */
.card{background:var(--bg-layer2);border:1px solid var(--line-deep);border-radius:var(--r-m)}
.card-h{display:flex;align-items:center;gap:var(--sp-s);flex-wrap:wrap;min-height:48px;padding:var(--sp-xs) var(--sp-m);border-bottom:1px solid var(--line-deep)}
.card-t{font-size:var(--font-size-body);font-weight:var(--font-weight-semibold);color:var(--text-heading)}
.card-m{font:var(--text-style-tips);color:var(--text-tips)}
.sp{flex:1}
.card-f{padding:var(--sp-s) var(--sp-m);border-top:1px solid var(--line-deep);font:var(--text-style-body);color:var(--text-2)}
.card-f b{color:var(--text-body);font-weight:var(--font-weight-semibold)}

/* KPI tiles. Emphasis on the primary tile is a surface-layer step, NOT the 4px
   brand rail — that rail is the selected indicator's vocabulary (R19 / M38). */
.kpi-row{display:grid;grid-template-columns:repeat(4,minmax(0,1fr))}
.kpi{display:flex;flex-direction:column;gap:var(--sp-xxs);padding:var(--sp-m) var(--sp-l);border-right:1px solid var(--line-deep)}
.kpi:last-child{border-right:0}
.kpi--p{background:var(--bg-layer3)}
.kpi-l{font-size:var(--font-size-tips);font-weight:var(--font-weight-semibold);color:var(--text-tips);text-transform:uppercase;letter-spacing:.7px}
.kpi--p .kpi-l{color:var(--text-2)}
.kpi-v{font:var(--text-style-large-title-24);color:var(--text-heading);font-variant-numeric:tabular-nums}
.kpi-v.good{color:var(--brand)}
.kpi-m{font:var(--text-style-tips);color:var(--text-tips);font-variant-numeric:tabular-nums}
.kpi-d{margin-top:var(--sp-xxs)}

/* chart — visual spec quoted from src/components/Chart/build-chart-option.ts
   and use-chart-tokens.ts (palette --chart-color-*, legend bottom + 8px circle
   + --text-2 @12px, axis/tick/grid --line-divider-light, labels --text-2) */
.chart-wrap{position:relative;padding:var(--sp-m) var(--sp-m) 0}
.chart-wrap svg{display:block;touch-action:none}
.chart-wrap:focus-visible{outline:2px solid var(--brand);outline-offset:-2px;border-radius:var(--r-s)}
.gridline,.axisline{stroke:var(--line-divider-light);stroke-width:1}
.axistext{fill:var(--text-2);font-family:var(--font-family-base);font-size:12px}
.ay{text-anchor:end}.ax{text-anchor:middle}
.s-cur{fill:none;stroke:var(--chart-color-1);stroke-width:2.5;stroke-linejoin:round;stroke-linecap:round}
.s-prev{fill:none;stroke:var(--chart-color-3);stroke-width:2;stroke-dasharray:6 4}
.t-line{stroke:var(--orange);stroke-width:1;stroke-dasharray:4 4}
.t-text{fill:var(--orange);font-family:var(--font-family-base);font-size:12px}
.d-cur{fill:var(--chart-color-1)}.d-prev{fill:var(--chart-color-3)}
.xhair{stroke:var(--text-tips);stroke-width:1;stroke-dasharray:3 3}
.legend{display:flex;align-items:center;gap:var(--sp-l);flex-wrap:wrap;padding:var(--sp-s) var(--sp-m) var(--sp-m)}
.lg{display:inline-flex;align-items:center;gap:var(--sp-xs);font:var(--text-style-tips);color:var(--text-2)}
.lg i{width:8px;height:8px;border-radius:50%;flex:none}
.lg .c1{background:var(--chart-color-1)}.lg .c3{background:var(--chart-color-3)}
.lg .tg{width:14px;height:0;border-radius:0;border-top:1px dashed var(--orange)}

/* table — mirror src/components/Table/Table.vue */
.tbl-wrap{padding:var(--sp-xs) var(--sp-m) var(--sp-s);overflow-x:auto}
.tbl{width:100%;background:var(--bg-layer2)}
.tbl th{padding:var(--sp-xs) var(--sp-xs) var(--sp-xs) 0;font:var(--text-style-body);font-weight:700;color:var(--text-tips);background:transparent;border-bottom:1px solid var(--line-deep);white-space:nowrap}
/* INFRA-F129 (1): neutralise the global `td{border-top:1px solid var(--line-border)}`
   baseline — under border-collapse it beats the component's own row border and
   makes the Vue and web-component chains draw different divider colours. */
.tbl td{border-top:0;border-bottom:1px solid var(--line-light);padding:var(--sp-xs) var(--sp-xs) var(--sp-xs) 0;font:var(--text-style-body);color:var(--text-body)}
.tbl tbody tr:last-child td{border-bottom:none}
.tbl tbody tr:hover td{background:var(--bg-layer3)}
.tbl .num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.tbl .app{width:40%;min-width:170px}
/* R23 (a) truncation CSS — (b) detection + (c) tooltip/title are in JS */
.ell{display:block;max-width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dl{display:inline-flex;align-items:center;gap:var(--sp-xxs);justify-content:flex-end;font-variant-numeric:tabular-nums}
.dl-improve{color:var(--brand)}.dl-worsen{color:var(--orange)}.dl-flat{color:var(--text-tips)}

/* Explainer panel.
   LIBRARY GAP: the published library has no collapsible panel / accordion
   (49 component_sets + the skeleton table both miss it). Built from tokens +
   library chevrons; candidate for design-system intake. */
.panel{position:sticky;top:var(--sp-l);background:var(--bg-layer2);border:1px solid var(--line-deep);border-radius:var(--r-m);overflow:hidden}
.panel-h{display:flex;align-items:center;gap:var(--sp-xs);min-height:48px;padding:var(--sp-xs) var(--sp-xs) var(--sp-xs) var(--sp-m)}
.panel-t{font-size:var(--font-size-body);font-weight:var(--font-weight-semibold);color:var(--text-heading)}
.panel-x{display:flex;align-items:center;justify-content:center;width:32px;height:32px;margin-left:auto;flex:none;border:0;border-radius:var(--r-s);background:transparent;color:var(--icon-default);cursor:pointer;transition:background .15s,color .15s}
.panel-x:hover{background:var(--bg-layer3);color:var(--icon-active)}
.panel-lead{padding:0 var(--sp-m) var(--sp-s);margin:0;font:var(--text-style-tips);color:var(--text-tips)}
.panel-f{padding:var(--sp-s) var(--sp-m);border-top:1px solid var(--line-deep);font:var(--text-style-tips);color:var(--text-tips)}
.panel.is-collapsed .panel-h{flex-direction:column;padding:var(--sp-s) var(--sp-xs);gap:var(--sp-s);min-height:0}
.panel.is-collapsed .panel-t{writing-mode:vertical-rl;text-orientation:mixed}
.panel.is-collapsed .panel-x{margin-left:0}
.acc{display:flex;align-items:center;gap:var(--sp-xs);width:100%;padding:var(--sp-s) var(--sp-m);text-align:left;background:transparent;border:0;border-top:1px solid var(--line-deep);border-radius:0;font:var(--text-style-body);font-weight:var(--font-weight-semibold);color:var(--text-body);cursor:pointer;transition:background .15s}
.acc:hover{background:var(--bg-layer3)}
.chev{margin-left:auto;color:var(--icon-default);transition:transform .18s}
.acc[aria-expanded=true] .chev{transform:rotate(180deg)}
.acc-b{padding:0 var(--sp-m) var(--sp-m);font:var(--text-style-body);color:var(--text-2)}
.acc-b p{margin:0 0 var(--sp-xs)}
.acc-b ul{margin:0;padding-left:var(--sp-m)}
.acc-b li{margin-bottom:var(--sp-xxs)}
/* no monospace token exists in this system by design — tabular-nums instead */
.fml{padding:var(--sp-s);margin-bottom:var(--sp-xs);border:1px solid var(--line-light);border-radius:var(--r-s);background:var(--bg-layer3);color:var(--text-body);font-variant-numeric:tabular-nums;line-height:var(--line-height-body)}
.fml b{display:block;font-weight:var(--font-weight-semibold)}
.fml span{display:block;margin-top:var(--sp-xxs);color:var(--text-2)}
.split{border-left:2px solid var(--line-light);padding-left:var(--sp-s);margin-bottom:var(--sp-xs)}
.split.in{border-left-color:var(--brand)}
.split.out{border-left-color:var(--text-disabled)}
.split em{display:block;font-style:normal;font-size:var(--font-size-tips);font-weight:var(--font-weight-semibold);text-transform:uppercase;letter-spacing:.7px;color:var(--text-tips)}
.sig{width:100%}
.sig th,.sig td{padding:var(--sp-xxs) var(--sp-xs) var(--sp-xxs) 0;font:var(--text-style-tips);border:0;border-bottom:1px solid var(--line-deep)}
.sig th{color:var(--text-tips);font-weight:var(--font-weight-semibold)}
.sig td{color:var(--text-2)}
.sig tr:last-child td{border-bottom:0}

/* responsive — literals must be members of the --bp-* value set */
@media (max-width:1100px){
.page-grid,.page-grid.is-collapsed{grid-template-columns:minmax(0,1fr)}
.panel{position:static}
.panel.is-collapsed .panel-h{flex-direction:row;padding:var(--sp-xs) var(--sp-xs) var(--sp-xs) var(--sp-m);min-height:48px}
.panel.is-collapsed .panel-t{writing-mode:horizontal-tb}
.panel.is-collapsed .panel-x{margin-left:auto}
}
@media (max-width:960px){
.topbar{gap:var(--sp-m)}.topbar-menu{display:none}
.kpi-row{grid-template-columns:repeat(2,minmax(0,1fr))}
.kpi:nth-child(2){border-right:0}
.kpi:nth-child(1),.kpi:nth-child(2){border-bottom:1px solid var(--line-deep)}
}
@media (max-width:640px){
.page{padding:var(--sp-m)}
.kpi-row{grid-template-columns:minmax(0,1fr)}
.kpi{border-right:0;border-bottom:1px solid var(--line-deep)}
.kpi:last-child{border-bottom:0}
.sync{display:none}
}
@media (prefers-reduced-motion:reduce){*{transition:none!important;animation:none!important}}
</style>
</head>
<body>

<!-- Icon sprite. Every path copied verbatim from
     tvu-design-system/figma-data/published/icons/svg/** — R15 §icons forbids
     hand-drawing an icon. Source file names kept for traceability. -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
<symbol id="i-up" viewBox="0 0 16 16"><!-- Arrow/Up__1149_23391 --><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>
<symbol id="i-down" viewBox="0 0 16 16"><!-- Arrow/Down__1149_23407 --><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>
<symbol id="i-dbl-up" viewBox="0 0 16 16"><!-- Arrow/Double-up__1149_23074 — catalog: expanded card, click to collapse --><path d="M2.9543 12.8872C2.68521 13.0806 2.3103 13.0192 2.11691 12.7501C1.92352 12.481 1.98489 12.1061 2.25397 11.9127L7.47469 8.16064C7.78853 7.93509 8.21134 7.93509 8.52518 8.16064L13.746 11.9127C14.015 12.1061 14.0764 12.481 13.883 12.7501C13.6896 13.0192 13.3147 13.0806 13.0456 12.8872L8.11666 9.3448C8.04692 9.29468 7.95296 9.29468 7.88322 9.3448L2.9543 12.8872ZM2.9521 7.88722C2.68301 8.08061 2.3081 8.01925 2.11471 7.75016C1.92132 7.48108 1.98269 7.10617 2.25177 6.91278L7.47469 3.15911C7.78853 2.93356 8.21134 2.93356 8.52518 3.15911L13.7482 6.91278C14.0172 7.10616 14.0786 7.48107 13.8852 7.75016C13.6918 8.01925 13.3169 8.08061 13.0478 7.88722L8.11666 4.34327C8.04691 4.29315 7.95296 4.29315 7.88322 4.34327L2.9521 7.88722Z" fill="currentColor"/></symbol>
<symbol id="i-dbl-down" viewBox="0 0 16 16"><!-- Arrow/Double-down__1149_23106 — catalog: collapsed card, click to expand --><path d="M2.94667 3.11282C2.67758 2.91943 2.30267 2.9808 2.10928 3.24988C1.91589 3.51897 1.97726 3.89388 2.24634 4.08727L7.47471 7.84485C7.78855 8.07041 8.21136 8.07041 8.5252 7.84486L13.7536 4.08727C14.0227 3.89388 14.0841 3.51897 13.8907 3.24988C13.6973 2.9808 13.3224 2.91943 13.0533 3.11282L8.11668 6.66069C8.04694 6.71082 7.95298 6.71082 7.88324 6.66069L2.94667 3.11282ZM2.94667 8.11282C2.67758 7.91943 2.30267 7.9808 2.10928 8.24988C1.91589 8.51897 1.97726 8.89388 2.24634 9.08727L7.47471 12.8448C7.78855 13.0704 8.21135 13.0704 8.52519 12.8449L13.7536 9.08727C14.0227 8.89388 14.0841 8.51897 13.8907 8.24988C13.6973 7.9808 13.3224 7.91943 13.0533 8.11282L8.11667 11.6607C8.04693 11.7108 7.95297 11.7108 7.88323 11.6607L2.94667 8.11282Z" fill="currentColor"/></symbol>
<symbol id="i-chev" viewBox="0 0 16 16"><!-- Arrow/Dropdown__1150_23504 --><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>
</svg>

<!-- TopBar — M1: every TVU product page instances this component, never draws its own. -->
<header class="topbar">
  <div class="topbar-brand">
    <!-- icon/logo/TVU__1136_3266. The SVG export resolves the brand Variable to a
         literal hex; that is the registered divergence icon-variable-resolved-to-hex,
         so the value is kept exactly as exported. -->
    <svg class="tvu-logo" viewBox="0 0 16 16" role="img" aria-label="TVU"><path d="M15.9933 8.68971V8.6899V8.69001C15.9962 8.96317 16 9.31582 16 9.81499V13.9033C16 13.9033 16 16 13.6733 15.9997H7.99586L7.97996 16H7.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.9185 7.18106C15.946 7.45033 15.99 7.97375 15.99 8.25C15.99 8.37606 15.9915 8.51693 15.9933 8.68953ZM1.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.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">TVU SaaS Dashboard</span>
  </div>
  <!-- Menu slot. Nav uses Tab/Item Type=Line — single-select navigation semantics. -->
  <nav class="topbar-menu" aria-label="Primary">
    <div class="tab-list">
      <a class="tab-item tab-item--line" href="#">Overview</a>
      <a class="tab-item tab-item--line" href="#">Usage</a>
      <a class="tab-item tab-item--line" href="#" aria-current="page">Cost</a>
      <a class="tab-item tab-item--line" href="#">Billing</a>
    </div>
  </nav>
  <div class="topbar-right">
    <span class="badge badge--neutral">Aug 2026 billing period</span>
    <span class="sync"><i aria-hidden="true"></i>Data synced 09:40 CST</span>
  </div>
</header>

<main class="page">
  <!-- Page-level strip. Page title and range controls sit BELOW the top bar,
       never inside its slots (catalog: Top bar / Don't use for). -->
  <div class="page-head">
    <div>
      <h1>Cost per Token</h1>
      <p>Blended delivery cost per 1,000 tokens, and whether it is improving.</p>
    </div>
    <!-- Tab List / Tab Item Type=Filled is the design system's segmented control.
         Not Button: "pick one of a set" is tab semantics, not an action. -->
    <div class="tab-list" role="tablist" aria-label="Time range" id="tabs">
      <button class="tab-item tab-item--filled" role="tab" data-r="7D" aria-selected="false">7D</button>
      <button class="tab-item tab-item--filled" role="tab" data-r="30D" aria-selected="true">30D</button>
      <button class="tab-item tab-item--filled" role="tab" data-r="90D" aria-selected="false">90D</button>
      <button class="tab-item tab-item--filled" role="tab" data-r="12M" aria-selected="false">12M</button>
    </div>
  </div>

  <div class="page-grid" id="grid">
    <div class="col-main">

      <!-- 1 · Overview -->
      <section class="card" aria-labelledby="ovT">
        <div class="card-h">
          <span class="card-t" id="ovT">Cost per 1,000 tokens</span>
          <span class="card-m" id="ovRange"></span>
          <span class="sp"></span>
          <span id="ovVerdict"></span>
        </div>
        <div class="kpi-row" id="kpi" aria-live="polite"></div>
        <div class="card-f" id="ovRead"></div>
      </section>

      <!-- 2 · Trend -->
      <section class="card" aria-labelledby="chT">
        <div class="card-h">
          <span class="card-t" id="chT">Trend</span>
          <span class="card-m" id="chGrain"></span>
          <span class="sp"></span>
          <span class="card-m">Target $0.0450 / 1K</span>
        </div>
        <!-- LIBRARY GAP (declared, not hidden): the design system's
             <Chart type="line"> wraps ECharts and cannot run in a single file
             with no external request, so the plot below is inline SVG. Its
             visual contract is quoted value-for-value from
             src/components/Chart/build-chart-option.ts + use-chart-tokens.ts,
             and the data structure below mirrors the component's
             {labels, datasets} props so this is a drop-in swap.
             Second gap found while doing it: Chart exposes no threshold /
             markLine prop, so the budget target line has no component path. -->
        <div class="chart-wrap" id="cw" tabindex="0" role="img" aria-describedby="cd">
          <div class="tooltip-box" id="tip" hidden></div>
        </div>
        <div class="legend">
          <span class="lg"><i class="c1"></i>Current period</span>
          <span class="lg"><i class="c3"></i>Previous period</span>
          <span class="lg"><i class="tg"></i>Budget target</span>
        </div>
        <p class="vh" id="cd"></p>
      </section>

      <!-- 3 · Cost drivers -->
      <section class="card" aria-labelledby="dvT">
        <div class="card-h">
          <span class="card-t" id="dvT">Cost drivers</span>
          <span class="sp"></span>
          <span class="card-m">Unit cost by workload, same period</span>
        </div>
        <div class="tbl-wrap">
          <table class="tbl">
            <thead><tr>
              <th class="app" scope="col">Workload</th>
              <th class="num" scope="col">Tokens</th>
              <th class="num" scope="col">Cost / 1K</th>
              <th class="num" scope="col">&#916; vs prev</th>
              <th class="num" scope="col">Share of spend</th>
            </tr></thead>
            <tbody id="dvB"></tbody>
          </table>
        </div>
      </section>
    </div>

    <!-- 4 · Collapsible explainer -->
    <aside class="panel" id="panel" aria-labelledby="pT">
      <div class="panel-h">
        <span class="panel-t" id="pT">How this is calculated</span>
        <button class="panel-x" id="pX" aria-expanded="true" aria-controls="pB" title="Collapse panel">
          <svg class="ico" aria-hidden="true"><use id="pIco" href="#i-dbl-up"/></svg>
          <span class="vh" id="pLbl">Collapse panel</span>
        </button>
      </div>
      <div id="pB">
        <p class="panel-lead">Every number on this page comes from the definitions below.</p>

        <button class="acc" aria-expanded="true" aria-controls="a1">Formula <svg class="ico chev" aria-hidden="true"><use href="#i-chev"/></svg></button>
        <div class="acc-b" id="a1">
          <div class="fml"><b>Cost per 1K tokens = Billable cost &#247; (Delivered tokens &#247; 1,000)</b><span id="f1"></span></div>
          <p>The rate is blended across the whole period, not an average of daily rates &#8212; heavier days weigh more.</p>
        </div>

        <button class="acc" aria-expanded="false" aria-controls="a2">What counts as a delivered token <svg class="ico chev" aria-hidden="true"><use href="#i-chev"/></svg></button>
        <div class="acc-b" id="a2" hidden><ul>
          <li>Counted at the egress boundary, after transcode &#8212; not at ingest.</li>
          <li>Retries inside one session count once.</li>
          <li>Internal health checks and synthetic monitoring are excluded.</li>
          <li>Attributed to the workload that produced the egress, not the account that owns the device.</li>
        </ul></div>

        <button class="acc" aria-expanded="false" aria-controls="a3">What sits inside billable cost <svg class="ico chev" aria-hidden="true"><use href="#i-chev"/></svg></button>
        <div class="acc-b" id="a3" hidden>
          <div class="split in"><em>Included</em><p>Compute, egress bandwidth, session-artifact storage (30-day retention), third-party AI inference.</p></div>
          <div class="split out"><em>Excluded</em><p>Platform licence fees, support plans, one-off onboarding, and anything billed outside the usage ledger.</p></div>
        </div>

        <button class="acc" aria-expanded="false" aria-controls="a4">How periods are compared <svg class="ico chev" aria-hidden="true"><use href="#i-chev"/></svg></button>
        <div class="acc-b" id="a4" hidden>
          <p id="f2"></p>
          <p>Both windows are the same length, so a delta never mixes a long period with a short one. The current period always ends at the last complete day.</p>
        </div>

        <button class="acc" aria-expanded="false" aria-controls="a5">Avoided cost <svg class="ico chev" aria-hidden="true"><use href="#i-chev"/></svg></button>
        <div class="acc-b" id="a5" hidden>
          <div class="fml"><b>Avoided cost = Delivered tokens &#247; 1,000 &#215; (previous rate &#8722; current rate)</b><span id="f3"></span></div>
          <p>It answers &#8220;what would this volume have cost at last period&#8217;s rate&#8221;. It is not a cash saving and never appears on an invoice.</p>
        </div>

        <button class="acc" aria-expanded="false" aria-controls="a6">How direction is shown <svg class="ico chev" aria-hidden="true"><use href="#i-chev"/></svg></button>
        <div class="acc-b" id="a6" hidden>
          <table class="sig">
            <thead><tr><th scope="col">State</th><th scope="col">Icon</th><th scope="col">Colour token</th></tr></thead>
            <tbody>
              <tr><td>Improving &#8212; cost fell</td><td>Down arrow</td><td>--brand</td></tr>
              <tr><td>Worsening &#8212; cost rose</td><td>Up arrow</td><td>--orange</td></tr>
              <tr><td>Flat &#8212; within &#177;1.0%</td><td>None</td><td>--text-tips</td></tr>
            </tbody>
          </table>
          <p style="margin-top:var(--sp-xs)">Volume never takes a direction colour: more tokens is neither good nor bad on its own.</p>
        </div>

        <button class="acc" aria-expanded="false" aria-controls="a7">Data freshness <svg class="ico chev" aria-hidden="true"><use href="#i-chev"/></svg></button>
        <div class="acc-b" id="a7" hidden><ul>
          <li>Usage pipeline last synced 09:40 CST.</li>
          <li>Same-day figures lag roughly 2 hours and can still move.</li>
          <li>Records reconcile against the billing ledger nightly; reconciled days do not change again.</li>
        </ul></div>

        <div class="panel-f">Definition v2.3 &#183; owner: Billing Platform &#183; last changed Jul 14, 2026</div>
      </div>
    </aside>
  </div>
</main>

<script>
(function(){
'use strict';
/* R16 — product UI strings in one map, single language, ready for an i18n layer. */
var T={improving:'Improving',worsening:'Worsening',flat:'Flat',rose:'rose',fell:'fell',flatv:'held flat',
        collapse:'Collapse panel',expand:'Expand panel'};
var MON=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
var N=730, TARGET=0.045, FLAT=0.01, END=new Date(2026,7,26);

/* Deterministic ledger: one row per day with two independent drivers (unit rate,
   delivered volume). Every KPI, table cell and both chart series derive from
   these rows, so no two numbers on the page can contradict each other. */
var D=[],i,t,d,dow,wk,rate,tok;
for(i=0;i<N;i++){
  t=i/(N-1);
  d=new Date(END.getFullYear(),END.getMonth(),END.getDate()-(N-1-i));
  dow=d.getDay(); wk=(dow===0||dow===6)?0.82:1;
  rate=0.0412+0.0378*Math.pow(1-t,0.55)+0.0009*Math.sin(i*0.8)+0.0004*Math.sin(i*0.23);
  tok=11.08e6*Math.exp(2.536*t)*wk*(1+0.06*Math.sin(i*0.9));
  D.push({d:d,rate:rate,tok:tok,spend:tok/1000*rate});
}
var R={'7D':{n:7,l:'Last 7 days',g:'day'},'30D':{n:30,l:'Last 30 days',g:'day'},
       '90D':{n:90,l:'Last 90 days',g:'day'},'12M':{n:365,l:'Last 12 months',g:'month'}};
/* Workload mix: shares sum to 1 and the multipliers blend back to the period
   rate, so the table reconciles with the KPI row by construction. */
var APPS=[{n:'Live Sports Encoding \u2014 EU-West',s:0.387,m:0.907,p:1.020},
          {n:'Cloud Playout',s:0.248,m:0.959,p:0.865},
          {n:'Remote Production Bridge',s:0.205,m:1.167,p:1.000},
          {n:'AI Captioning (beta)',s:0.160,m:1.071,p:1.075}];
var range='30D', geo=null, ser=null;

function stats(a,b){var tk=0,sp=0,j;for(j=a;j<b;j++){tk+=D[j].tok;sp+=D[j].spend;}
  return{from:a,to:b,tok:tk,spend:sp,rate:sp/(tk/1000)};}
function win(){var r=R[range];return{cur:stats(N-r.n,N),prev:stats(N-2*r.n,N-r.n),n:r.n,g:r.g,l:r.l};}
function months(a,b){var o=[],k=null,c=null,j,kk;
  for(j=a;j<b;j++){kk=D[j].d.getFullYear()+'-'+D[j].d.getMonth();
    if(kk!==k){k=kk;c={l:MON[D[j].d.getMonth()],tok:0,sp:0};o.push(c);} c.tok+=D[j].tok;c.sp+=D[j].spend;}
  /* first bucket is a partial month on both sides of the comparison -> drop it */
  return o.slice(1).map(function(b2){return{l:b2.l,v:b2.sp/(b2.tok/1000)};});}
function series(w){var j,L=[],c=[],p=[];
  if(w.g==='month'){var a=months(w.cur.from,w.cur.to),b=months(w.prev.from,w.prev.to);
    return{L:a.map(function(x){return x.l;}),c:a.map(function(x){return x.v;}),p:b.map(function(x){return x.v;})};}
  for(j=w.cur.from;j<w.cur.to;j++){L.push(MON[D[j].d.getMonth()]+' '+D[j].d.getDate());c.push(D[j].rate);}
  for(j=w.prev.from;j<w.prev.to;j++)p.push(D[j].rate);
  return{L:L,c:c,p:p};}
function cls(x){return Math.abs(x)<FLAT?'flat':(x<0?'improve':'worsen');}  /* lower is better */

function r4(v){return '$'+v.toFixed(4);}
function mny(v){return v>=1e6?'$'+(v/1e6).toFixed(2)+'M':v>=1e3?'$'+(v/1e3).toFixed(1)+'K':'$'+v.toFixed(0);}
function tk(v){return v>=1e9?(v/1e9).toFixed(2)+'B':v>=1e6?(v/1e6).toFixed(1)+'M':Math.round(v).toLocaleString('en-US');}
function pc(v){return (v>0?'+':v<0?'\u2212':'')+Math.abs(v*100).toFixed(1)+'%';}
function ap(v){return Math.abs(v*100).toFixed(1)+'%';}
function fd(x){return MON[x.getMonth()]+' '+x.getDate()+', '+x.getFullYear();}
function gp(v){return Math.round(v).toLocaleString('en-US');}

/* M11 — direction is a two-layer signal: arrow icon AND coloured number.
   Tokens: --brand / --orange / --text-tips. Icon threshold: +/-1.0% (FLAT). */
function badge(x){var s=cls(x);
  if(s==='flat')return '<span class="badge badge--neutral">'+pc(x)+' \u00b7 '+T.flat+'</span>';
  return '<span class="badge badge--'+(s==='improve'?'green':'orange')+'">'+
    '<svg class="ico" aria-hidden="true"><use href="'+(s==='improve'?'#i-down':'#i-up')+'"/></svg>'+
    pc(x)+' \u00b7 '+(s==='improve'?T.improving:T.worsening)+'</span>';}
function dcell(x){var s=cls(x);
  return '<span class="dl dl-'+s+'">'+(s==='flat'?'':
    '<svg class="ico" aria-hidden="true"><use href="'+(s==='improve'?'#i-down':'#i-up')+'"/></svg>')+pc(x)+'</span>';}
function tile(l,v,m,pri,bd,good){
  return '<div class="kpi'+(pri?' kpi--p':'')+'"><span class="kpi-l">'+l+'</span>'+
    '<span class="kpi-v'+(good?' good':'')+'">'+v+'</span>'+
    (bd?'<span class="kpi-d">'+bd+'</span>':'')+'<span class="kpi-m">'+m+'</span></div>';}

var $=function(x){return document.getElementById(x);};
var cw=$('cw'), tip=$('tip');

function render(){
  var w=win(); ser=series(w);
  var dr=w.cur.rate/w.prev.rate-1, ds=w.cur.spend/w.prev.spend-1, dt=w.cur.tok/w.prev.tok-1;
  var av=(w.cur.tok/1000)*(w.prev.rate-w.cur.rate);
  $('ovRange').textContent=w.l;
  $('ovVerdict').innerHTML=badge(dr);
  $('kpi').innerHTML=
    tile('Cost per 1K tokens',r4(w.cur.rate),'Previous period '+r4(w.prev.rate),true,'')+
    tile('Total spend',mny(w.cur.spend),'Previous period '+mny(w.prev.spend),false,badge(ds))+
    tile('Tokens delivered',tk(w.cur.tok),'Previous period '+tk(w.prev.tok)+' \u00b7 '+pc(dt)+
         ' \u00b7 context metric, no direction',false,'')+
    tile(av>=0?'Avoided cost':'Extra cost',mny(Math.abs(av)),
         'Versus holding the previous unit rate',false,'',av>=0);
  var wd=function(v){return cls(v)==='flat'?T.flatv:(v<0?T.fell:T.rose);};
  var am=function(v){return cls(v)==='flat'?'':' '+ap(v);};
  $('ovRead').innerHTML='<b>Reading.</b> Unit cost '+wd(dr)+am(dr)+' against the previous '+
    (w.g==='month'?'12 months':w.n+' days')+'. Total spend '+wd(ds)+am(ds)+
    ' while delivered volume '+wd(dt)+am(dt)+'.';

  var rows=APPS.map(function(a){var q=w.cur.tok*a.s, rr=w.cur.rate*a.m, pr=w.prev.rate*a.p;
    return{n:a.n,tok:q,rate:rr,spend:q/1000*rr,d:rr/pr-1};});
  var tot=rows.reduce(function(s,r){return s+r.spend;},0);
  $('dvB').innerHTML=rows.map(function(r){return '<tr>'+
    '<td class="app"><span class="ell">'+r.n+'</span></td>'+
    '<td class="num">'+tk(r.tok)+'</td><td class="num">'+r4(r.rate)+'</td>'+
    '<td class="num">'+dcell(r.d)+'</td>'+
    '<td class="num">'+(r.spend/tot*100).toFixed(1)+'%</td></tr>';}).join('');
  truncate();

  $('f1').textContent=mny(w.cur.spend)+' \u00f7 ('+gp(w.cur.tok)+' \u00f7 1,000) = '+r4(w.cur.rate)+' per 1K';
  $('f3').textContent=gp(w.cur.tok/1000)+' \u00d7 ('+r4(w.prev.rate)+' \u2212 '+r4(w.cur.rate)+
    ') = '+mny(Math.abs(av));
  $('f2').textContent='Current period: '+fd(D[w.cur.from].d)+' \u2013 '+fd(D[w.cur.to-1].d)+
    '. Previous period: '+fd(D[w.prev.from].d)+' \u2013 '+fd(D[w.prev.to-1].d)+'.';
  $('chGrain').textContent=w.g==='month'
    ?'One point = one calendar month, latest is month-to-date':'One point = one day';
  $('cd').textContent='Line chart. Cost per 1,000 tokens over the '+w.l.toLowerCase()+
    ', from '+r4(ser.c[0])+' to '+r4(ser.c[ser.c.length-1])+', against a previous-period line and a '+
    r4(TARGET)+' target. Period blend '+r4(w.cur.rate)+', '+pc(dr)+' versus the previous period.';
  draw();
}

function step(raw){var mg=Math.pow(10,Math.floor(Math.log10(raw))),n2=raw/mg;
  return (n2<=1?1:n2<=2?2:n2<=2.5?2.5:n2<=5?5:10)*mg;}
function draw(){
  /* viewBox is set to the measured pixel width so 1 unit = 1px: axis text stays
     exactly 12px and stroke widths stay true at any container width. */
  var W=Math.max(320,cw.clientWidth-32), H=300, PL=64,PR=20,PT=16,PB=44;
  var pw=W-PL-PR, ph=H-PT-PB, n=ser.c.length, m=Math.min(n,ser.p.length);
  var all=ser.c.concat(ser.p.slice(0,m)).concat([TARGET]);
  var lo=Math.min.apply(null,all), hi=Math.max.apply(null,all);
  var pad=(hi-lo)*0.15||0.001, st=step((hi-lo+2*pad)/4);
  lo=Math.floor((lo-pad)/st)*st; hi=Math.ceil((hi+pad)/st)*st;
  var sx=n>1?pw/(n-1):0;
  var X=function(k){return PL+k*sx;}, Y=function(v){return PT+ph*(1-(v-lo)/(hi-lo));};
  geo={X:X,Y:Y,n:n,m:m,PT:PT,ph:ph,PL:PL,W:W};
  var s='',v,k;
  for(v=lo;v<=hi+1e-9;v+=st){var y=Y(v);
    s+='<line class="gridline" x1="'+PL+'" y1="'+y.toFixed(1)+'" x2="'+(W-PR)+'" y2="'+y.toFixed(1)+'"/>'+
       '<text class="axistext ay" x="'+(PL-10)+'" y="'+(y+4).toFixed(1)+'">'+r4(v)+'</text>';}
  s+='<line class="axisline" x1="'+PL+'" y1="'+(PT+ph)+'" x2="'+(W-PR)+'" y2="'+(PT+ph)+'"/>';
  var ls=Math.ceil(n/8);
  for(k=0;k<n;k+=ls)s+='<text class="axistext ax" x="'+X(k).toFixed(1)+'" y="'+(PT+ph+20)+'">'+ser.L[k]+'</text>';
  var ty=Y(TARGET);
  s+='<line class="t-line" x1="'+PL+'" y1="'+ty.toFixed(1)+'" x2="'+(W-PR)+'" y2="'+ty.toFixed(1)+'"/>'+
     '<text class="t-text" x="'+PL+'" y="'+(ty-6).toFixed(1)+'">Target '+r4(TARGET)+'</text>';
  var pts=function(a,len){var o=[],j;for(j=0;j<len;j++)o.push(X(j).toFixed(1)+','+Y(a[j]).toFixed(1));return o.join(' ');};
  if(m>1)s+='<polyline class="s-prev" points="'+pts(ser.p,m)+'"/>';
  if(n>1)s+='<polyline class="s-cur" points="'+pts(ser.c,n)+'"/>';
  s+='<circle class="d-cur" cx="'+X(n-1).toFixed(1)+'" cy="'+Y(ser.c[n-1]).toFixed(1)+'" r="4"/>';
  s+='<line class="xhair" id="xh" x1="0" y1="'+PT+'" x2="0" y2="'+(PT+ph)+'" style="display:none"/>'+
     '<circle class="d-cur" id="hc" r="4" style="display:none"/>'+
     '<circle class="d-prev" id="hp" r="4" style="display:none"/>';
  cw.insertAdjacentHTML('afterbegin','');
  var old=cw.querySelector('svg'); if(old)old.remove();
  var svg='<svg viewBox="0 0 '+W+' '+H+'" width="100%" height="'+H+'" aria-hidden="true">'+s+'</svg>';
  cw.insertAdjacentHTML('afterbegin',svg);
}

var hoverIdx=-1;
function hover(k){
  if(!geo)return;
  k=Math.max(0,Math.min(geo.n-1,k)); hoverIdx=k;
  var xh=$('xh'),hc=$('hc'),hp=$('hp'),x=geo.X(k);
  xh.setAttribute('x1',x);xh.setAttribute('x2',x);xh.style.display='';
  hc.setAttribute('cx',x);hc.setAttribute('cy',geo.Y(ser.c[k]));hc.style.display='';
  var has=k<geo.m;
  if(has){hp.setAttribute('cx',x);hp.setAttribute('cy',geo.Y(ser.p[k]));hp.style.display='';}
  else hp.style.display='none';
  tip.innerHTML='<div class="tt-k">'+ser.L[k]+'</div>'+
    '<div class="tt-r"><i style="background:var(--chart-color-1)"></i>Current '+r4(ser.c[k])+'</div>'+
    (has?'<div class="tt-r"><i style="background:var(--chart-color-3)"></i>Previous '+r4(ser.p[k])+'</div>':'');
  tip.hidden=false;
  var tw=tip.offsetWidth, left=x+16-16;         /* wrap has 16px left padding */
  if(left+tw>geo.W-8)left=x-tw-8;
  tip.style.left=Math.max(0,left)+'px';
  tip.style.top=(geo.Y(ser.c[k])-tip.offsetHeight-10+16)+'px';
}
function unhover(){tip.hidden=true;var a=$('xh'),b=$('hc'),c=$('hp');
  if(a)a.style.display='none';if(b)b.style.display='none';if(c)c.style.display='none';hoverIdx=-1;}
cw.addEventListener('pointermove',function(e){
  if(!geo)return;
  var r=cw.getBoundingClientRect();
  hover(Math.round((e.clientX-r.left-(geo.n>1?geo.PL:0))/((geo.W-geo.PL-20)/Math.max(1,geo.n-1))));
});
cw.addEventListener('pointerleave',unhover);
cw.addEventListener('keydown',function(e){
  if(e.key==='ArrowRight'){e.preventDefault();hover((hoverIdx<0?geo.n-1:hoverIdx)+1);}
  else if(e.key==='ArrowLeft'){e.preventDefault();hover((hoverIdx<0?geo.n-1:hoverIdx)-1);}
  else if(e.key==='Escape')unhover();
});
cw.addEventListener('blur',unhover);

/* R23 (b) truncation detection + (c) tooltip / title fallback — only cells that
   actually overflow get one, so short names never fire a pointless tooltip. */
function truncate(){
  var cells=document.querySelectorAll('#dvB .ell'),j;
  for(j=0;j<cells.length;j++){
    var el=cells[j];
    if(el.scrollWidth>el.clientWidth)el.setAttribute('title',el.textContent);
    else el.removeAttribute('title');
  }
}

/* range switch */
$('tabs').addEventListener('click',function(e){
  var b=e.target.closest('[data-r]'); if(!b)return;
  range=b.getAttribute('data-r');
  var all=this.querySelectorAll('[data-r]'),j;
  for(j=0;j<all.length;j++)all[j].setAttribute('aria-selected',all[j]===b?'true':'false');
  unhover(); render();
});

/* accordion — hidden attribute, not an off-screen corpse (R14) */
document.addEventListener('click',function(e){
  var b=e.target.closest('.acc'); if(!b)return;
  var open=b.getAttribute('aria-expanded')==='true';
  b.setAttribute('aria-expanded',open?'false':'true');
  document.getElementById(b.getAttribute('aria-controls')).hidden=open;
});

/* panel collapse — Double-up = expanded, Double-down = collapsed (catalog) */
$('pX').addEventListener('click',function(){
  var open=this.getAttribute('aria-expanded')==='true';
  this.setAttribute('aria-expanded',open?'false':'true');
  this.title=open?T.expand:T.collapse;
  $('pLbl').textContent=open?T.expand:T.collapse;
  $('pIco').setAttribute('href',open?'#i-dbl-down':'#i-dbl-up');
  $('pB').hidden=open;
  $('panel').classList.toggle('is-collapsed',open);
  $('grid').classList.toggle('is-collapsed',open);
  requestAnimationFrame(function(){unhover();draw();truncate();});
});

/* re-render the plot whenever its box changes (window resize, panel collapse) */
var queued=false;
if(window.ResizeObserver)new ResizeObserver(function(){
  if(queued)return; queued=true;
  requestAnimationFrame(function(){queued=false;unhover();draw();truncate();});
}).observe(cw);

render();
})();
</script>
</body>
</html>
```

**几个用得上的验证点**：所有数字（KPI、表格、面板里的算式、图上两条线）都由同一份日粒度合成账算出，换范围时不可能对不上；"Reading" 那句话的措辞由涨跌符号推导，不会出现"文字说降、数字是涨"。方向信号按 M11 双层：箭头 icon + 数字染色，阈值 ±1.0% 走 Flat（无 icon、中性色），三态在表格里都能看到实例（-20.0% 绿 / -0.2% Flat / +5.0% 橙）。

**留给 owner 的两条建议立项**：① `Chart` 缺 threshold / markLine prop —— 预算目标线这类"是否达标"的锚在组件层无路可走；② 库里没有 collapsible panel / accordion，本页自建并已标 `LIBRARY GAP` 注释。另外 `domain-tvu.md` M11 那行的 `--text-warning` 是个悬挂 token 名，建议改成 `--orange`。