我已完成 onboarding 链路（FIGMA_AS_SOURCE_OF_TRUTH → PROJECT_GOAL → AGENTS 硬规则 → code-conventions 起手段 + R2/R3/R16/R19/R23 → domain-tvu M3-M11 → mockup-conventions M29/M41），并逐个把要用到的组件真源读到手：`variables.css`（54 个 token 全量）、`Button.vue`（PALETTE_SPEC + SIZE_GEOMETRY）、`Pill.vue` + `PillStatus.vue`（发布态 8 变体）、`Table.vue`（CANONICAL-029 padding）、`Notification.vue`（pop confirm）、`PopupBox.vue`（scrim/焦点陷阱）、`TopBar.vue`、`MenuList.vue`，以及 `dist/icons/svg/` 里 12 个实际图标文件。

## 关键设计决策（都有真源，不是我的偏好）

**1. 正常态是红色，不是绿色。** `domain-tvu.md` M3：广播行业 ON AIR 惯例 —— Live=红 / Preview-Standby=绿 / Inactive=灰；`code-conventions.md` §Convention Priority 逐字拿这条当例子（「M3 红=Live (P1) vs 通用 UX 红=危险 (P3) → M3 胜出」）。canonical `PillStatus.vue` 发布态也是 `On-air → --red`。三态用 `hue@18% 底 + 1px hue 边 + hue 文字 + 6px dot`，另配文字标签与原因行做非颜色冗余。

**2. 没有「新建直播源」按钮。** M5：监控/操控台类页面不暴露创建入口，只做查看/启停/跳转/移除。你的需求清单里也没有它。

**3. 只交付 dark。** M4：监控类 deliverable 默认仅 dark mode，不画 light 对比。

**4. 产品 UI 走英文单语 + `const T` 映射表。** R16/M33：产品 UI 串禁中英混排、必须走 i18n key / const map；M9 要求行内动作用 literal 动词（`Stop Stream` / `Start Stream` / `Assign Receiver`）。

**5. 离线分两个子型。** M10：`Inactive-Idle`（`Last ran 08:12 → 09:47 · 95 min`）vs `Inactive-NeverStarted`（`Never started` + CTA `Assign Receiver`），不许合并成单一 Inactive。

**6. 二次确认用 Notification `form="pop confirm"` `type="danger"`。** M41 指定该组件、480 宽、居中单 dialog + 全屏 scrim；`type=danger` → confirm 按钮 filling red、无 category icon（组件契约）。焦点陷阱/Esc/焦点归还/backdrop 不关闭，全部照 `PopupBox.vue` 行为。

## 三个如实登记的偏差（都可复核）

| # | 事实 | 我怎么处理 |
|---|---|---|
| 1 | `PillStatus` 发布态只有 `On-air / Preview / Analyzing / Inactive` 四值，**没有 Warning** | 用同一构造式换 `--orange`，**这是硬规则 #3 意义上的未登记变体** —— 需要你拍板：要么在 Figma 补 `Pill Status.Status=Warning`，要么改用 `Analyzing`(blue)。我没静默发明 |
| 2 | scrim 值两处打架：M41 说 `rgba(0,0,0,0.2)`+blur8（2026-05-26 user-validated），`--mask-overlay` token 说 `0.6`+blur8（注释标「figma 真源 Mask blur8 #000-60%」，`PopupBox.vue` 在用） | 走 **token**（code 侧真源；M41 自己写着 code 镜像 R21「候选、待补」）。这条冲突值得你裁一次 |
| 3 | `Button.vue` PALETTE_SPEC 里 `ghost/red` hover 底色 = `--red-bg` = `#fff1f0`，而 `--red-bg` / `--orange-bg` 在 `:root` 与 `[data-theme=light]` **同值**（浅色） ⇒ dark 主题下 hover 会闪一块近白粉底 | **照 DS 原样实现**（FIGMA_AS_SOURCE_OF_TRUTH：不许 code 自行「修正」）。这是本次最值得回流的发现 —— 主色 destructive 按钮在 dark 下 hover 反白 |

另外两处小说明：`--font-family-clock`（Digital Numbers）我没用 —— 单文件不能引字体文件，退化成 Courier New 反而失真；DS 注释也明确「数据/数字用 `--font-family-base` + `tabular-nums`，clock 字体只留给 TopBar 时钟」，页面按后者做。行溢出按 R23 三件套实现（center-ellipsis JS + `scrollWidth` 真截断检测 + 仅真截断才挂 tooltip，tooltip 用 `--tooltip-*` token 而非自画配色）。空值按 M29 Form 2：`—` + placeholder 色 + **无** copy icon。

```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<title>Live Sources · TVU</title>

<style>
/* ==========================================================================
 * 1. DESIGN TOKENS
 * Verbatim copy of the dark (:root) layer of tvu-design-system
 * src/tokens/variables.css. Values are Figma-Variable-synced by `pnpm generate`
 * and gated by `pnpm audit:token-contract` — do NOT hand-edit them here or
 * upstream; change the design in Figma and re-sync.
 * In a build-enabled consumer this block is replaced by
 *   <link rel="stylesheet" href="tvu-tokens.css">   (see R2.1)
 * Light-mode overrides are deliberately omitted: domain-tvu.md M4 —
 * monitoring / operator-console deliverables ship dark-only.
 * ========================================================================== */
:root {
  /* ---- Neutrals ---- */
  --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;
  --orange-tz: #d39f39;
  --blue: #3892f3;
  --blue-hover: #62a9f6;
  --blue-bg: #e1eefd;

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

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

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

  /* ---- Surface ---- */
  --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 ---- */
  --line-border: #595959;
  --line-deep: #353535;
  --line-light: #434343;

  /* ---- 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 ---- */
  --z-dropdown: 1000;
  --z-tooltip: 1000;
  --z-modal: 1000;
  --z-select-portal: 1100;

  /* ---- Layout containers (audit:layout-tokens scale) ---- */
  --container-page: 1600px;
  --container-wide: 1800px;

  /* ---- Typography ----
     NOTE: --font-family-clock ('Digital Numbers') is intentionally NOT used in
     this file. It needs the self-hosted OFL font file, which a no-external-asset
     single file cannot carry, and variables.css scopes it to the top-bar clock
     face only: "Everything else — UI text AND data (IP / MAC / serial / dBm /
     byte counts) — uses --font-family-base ... for column-aligned digits add
     font-variant-numeric: tabular-nums". That is what the metrics below do. */
  --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);

  /* ---- Component-variant tokens (theme-stable) ---- */
  --notification-bg-dark: var(--color-grey-10);
  --notification-width-default: 480px;
  --notification-width-side-pop: 320px;
  --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;
  --topbar-section-gap: 80px;
  --topbar-height: 64px;
  --control-height-xxs: 32px;
  --form-item-row-height: 32px;

  /* ---- Effect styles (Figma 3-layer drop shadows) ---- */
  --shadow-l1:
    0 5px 12px 4px #00000017,
    0 3px 6px 0 #0000001f,
    0 1px 2px -2px #00000029;
  --shadow-l2:
    0 9px 28px 8px #0000000d,
    0 6px 16px 0 #00000014,
    0 3px 6px -4px #0000001f;
  --shadow-l3:
    0 12px 48px 16px #00000008,
    0 9px 28px 0 #0000000d,
    0 6px 16px -8px #00000014;
  --mask-overlay: rgba(0, 0, 0, 0.6);
  --mask-overlay-blur: 8px;

  /* ---- Text styles (Figma 7 Text Styles) ---- */
  --text-style-tips:           400 12px/16px var(--font-family-base);
  --text-style-body:           400 14px/21px var(--font-family-base);
  --text-style-pop-title:      600 16px/24px var(--font-family-base);
  --text-style-headline-2:     600 18px/27px var(--font-family-base);
  --text-style-headline-1:     600 20px/30px var(--font-family-base);
  --text-style-large-title-24: 600 24px/36px var(--font-family-base);
  --font-size-tips: 12px;
  --font-size-body: 14px;
  --line-height-tips: 16px;
  --font-weight-semibold: 600;

  /* ---- Derived aliases ---- */
  --input-filled-bg: var(--bg-layer4);
  --green-bg: var(--brand-match);
  --control-disabled-text: var(--text-disabled);
}

/* ==========================================================================
 * 2. ELEMENT BASE LAYER
 * Verbatim from variables.css "Element base layer" (spec
 * 2026-07-23-ds-rule-consumption-L5-enforcement, Tier 1). This is the DS's
 * correct-by-construction fallback for semantic HTML, which is exactly the
 * situation of a build-less single file: the toolbar <input> / <select> below
 * get DS type, colour, border and radius from here, and only take layout
 * (height / inline padding) from authored rules.
 * ========================================================================== */
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); }
button {
  font: var(--text-style-body);
  color: inherit;
  border-radius: var(--r-s);
}
input,
select {
  font: var(--text-style-body);
  color: var(--text-body);
  background: var(--input-filled-bg);
  border: 1px solid var(--line-border);
  border-radius: var(--r-s);
  padding: var(--sp-xs) var(--sp-s);
}
input::placeholder { color: var(--text-placeholder); }
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);
}
[role="dialog"] { font: var(--text-style-body); color: var(--text-body); }

/* ==========================================================================
 * 3. PAGE SHELL
 * ========================================================================== */
* { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg-layer1);
  color: var(--text-body);
  -webkit-font-smoothing: antialiased;
}

.page {
  max-width: var(--container-page);
  margin: 0 auto;
  padding: var(--sp-l) var(--sp-l) var(--sp-xxl);
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.icon {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  display: inline-block;
  vertical-align: middle;
}

/* ==========================================================================
 * 4. TOP BAR — 1:1 mirror of 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-menu {
  display: flex;
  align-items: center;
  gap: var(--sp-xxl);
  flex: 1;
}
.topbar-right {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  flex-shrink: 0;
  margin-left: auto;
}
/* Logo — mirror of src/components/Logo/Logo.vue (type="tvu", size prop). */
.logo {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 28px;
  height: 28px;
}
.logo svg { width: 100%; height: 100%; }

/* Page-level navigation — 1:1 mirror of MenuList.vue .menu-list--horizontal.
   Truth = the Menu sub-component inside the published Top bar (4771:7173):
   18px SemiBold, active differs from normal ONLY in text colour, container gap
   = --sp-m, item padding = 0 --sp-xs, and every instance has strokes: [].
   ⛔ There is NO underline / brand-colour active state in the design — a
   code-first invention shipped 2026-07-02 was reverted by owner ruling
   2026-08-14 ("以 Figma 为准"). Do not re-add it. */
.menu-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--sp-m);
}
.menu-list__item {
  display: flex;
  align-items: center;
  gap: var(--sp-xxs);
  padding: 0 var(--sp-xs);
  font: var(--text-style-headline-2);
  color: var(--text-tips);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  background: none;
  border: 0;
}
.menu-list__item:hover:not(.is-disabled) { color: var(--text-heading); }
.menu-list__item.is-active { color: var(--text-body); }
.menu-list__item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-height-xxs);
  height: var(--control-height-xxs);
  border-radius: 50%;
  background: var(--bg-layer4);
  color: var(--text-2);
  font-size: var(--font-size-tips);
  font-weight: var(--font-weight-semibold);
  letter-spacing: .02em;
  flex: 0 0 auto;
}

/* ==========================================================================
 * 5. BUTTON — 1:1 mirror of src/components/Button/Button.vue
 * SIZE_GEOMETRY + PALETTE_SPEC copied from that file. Consumer contract there
 * is canonical-style × canonical-color × canonical-size; here the same axes are
 * flattened into classes because a build-less page cannot import the SFC (R3:
 * "手写 1:1 mirror canonical 的 props / events / slots / 视觉").
 * ========================================================================== */
.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-opacity: 1;
  --btn-height: 32px;          /* SIZE_GEOMETRY.M */
  --btn-font-size: 14px;
  --btn-line-height: 21px;
  --btn-gap: 0px;
  --btn-padding-inline: 16px;
  --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);
  opacity: var(--btn-opacity);
  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 .15s, color .15s, border-color .15s, box-shadow .15s, opacity .15s;
  white-space: nowrap;
  outline: none;
  user-select: none;
}
.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; }

/* sizes */
.btn--s  { --btn-height: 24px; --btn-padding-inline: 8px; --btn-gap: 4px; }
.btn--xs { --btn-height: 16px; --btn-font-size: 12px; --btn-line-height: 14px;
           --btn-padding-inline: 4px; --btn-radius-square: 2px; }
/* icon-only square button: keeps the M control height, drops inline padding */
.btn--icon { --btn-padding-inline: 0; width: var(--control-height-xxs); }

/* PALETTE_SPEC.filling */
.btn--fill-green {
  --btn-bg: var(--brand);        --btn-color: var(--text-primary-btn); --btn-border: transparent;
  --btn-bg-hover: var(--brand-hover); --btn-color-hover: var(--text-primary-btn); --btn-border-hover: transparent;
}
.btn--fill-red {
  --btn-bg: var(--red);          --btn-color: var(--text-primary-btn); --btn-border: transparent;
  --btn-bg-hover: var(--red-hover);   --btn-color-hover: var(--text-primary-btn); --btn-border-hover: transparent;
}
.btn--fill-gray {
  --btn-bg: var(--bg-grey-btn-en); --btn-color: var(--text-2); --btn-border: transparent;
  --btn-bg-hover: var(--bg-grey-btn-hv); --btn-color-hover: var(--text-2); --btn-border-hover: transparent;
}
/* PALETTE_SPEC.ghost — note border-width 1.2px (Button.vue borderWidth computed) */
.btn--ghost-gray {
  --btn-border-width: 1.2px;
  --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);
}
/* ⚠️ FIDELITY NOTE (reported to owner, NOT silently "fixed"):
   PALETTE_SPEC.ghost.red.hover.bg is var(--red-bg), and --red-bg is #fff1f0 in
   BOTH :root and [data-theme="light"] — i.e. theme-invariant and light. So in
   dark mode a ghost/red button hovers to a near-white pink fill. Same shape for
   ghost/orange (--orange-bg #fff3db). Mirrored verbatim per
   FIGMA_AS_SOURCE_OF_TRUTH.md (code follows the DS; code does not invent a fix). */
.btn--ghost-red {
  --btn-border-width: 1.2px;
  --btn-bg: transparent; --btn-color: var(--red); --btn-border: var(--red);
  --btn-bg-hover: var(--red-bg); --btn-color-hover: var(--red); --btn-border-hover: var(--red);
}
.btn--ghost-green {
  --btn-border-width: 1.2px;
  --btn-bg: transparent; --btn-color: var(--brand); --btn-border: var(--brand);
  --btn-bg-hover: var(--green-bg); --btn-color-hover: var(--brand); --btn-border-hover: var(--brand);
}
/* PALETTE_SPEC.rimless */
.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;
}
/* PALETTE_SPEC[*][*].disable */
.btn--ghost-red:disabled {
  --btn-bg: transparent; --btn-color: var(--red); --btn-border: var(--red);
  --btn-opacity: .4;   /* Button.vue surfaceOpacity(): red/orange disable → 0.4 */
}
.btn--ghost-gray:disabled {
  --btn-color: var(--text-placeholder); --btn-border: var(--bg-grey-btn-en);
}

/* ==========================================================================
 * 6. PILL / STATUS — 1:1 mirror of src/components/Pill/Pill.vue geometry +
 * src/canonical/PillStatus.vue token maps (CANONICAL-028, published set
 * 5672:140617, fileKey YbsPRUVmNdsbN40NNwh1Gn).
 * Geometry: height 28 · padding 4/12/4/10 · gap 8 · radius pill · 12px/500.
 * Active=True bg is the raw hue at 18% reproduced via color-mix over the token
 * — NOT the solid --msg-bg-* tokens.
 * ========================================================================== */
.pill {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  gap: var(--sp-xs);
  padding: var(--sp-xxs) var(--sp-s) var(--sp-xxs) 10px;
  border-radius: 999px;
  background: var(--pill-bg);
  border: 1px solid var(--pill-border, transparent);
  color: var(--pill-text, var(--text-body));
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  font-family: var(--font-family-base);
  white-space: nowrap;
}
.pill__dot {
  flex-shrink: 0;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--pill-dot);
}
.pill__label { font-variant-numeric: tabular-nums; }
.pill__count { font-weight: 700; font-variant-numeric: tabular-nums; }

/* Status=On-air, Active=True — domain-tvu.md M3: red = Live (broadcast ON AIR
   convention). This is NOT the generic-SaaS "red = danger". */
.pill--on-air {
  --pill-bg: color-mix(in srgb, var(--red) 18%, transparent);
  --pill-border: var(--red);
  --pill-dot: var(--red);
  --pill-text: var(--red);
}
/* ⚠️ Status=Warning is NOT in the published 4-variant set
   (On-air / Preview / Analyzing / Inactive). Built from the same construction
   with --orange. Under AGENTS.md hard rule #3 this is an unregistered variant
   and needs an owner ruling: either add `Pill Status.Status=Warning` in Figma,
   or fall back to the published `Analyzing` (blue). Flagged, not hidden. */
.pill--warning {
  --pill-bg: color-mix(in srgb, var(--orange) 18%, transparent);
  --pill-border: var(--orange);
  --pill-dot: var(--orange);
  --pill-text: var(--orange);
}
/* Status=Inactive, Active=True — per PillStatus.vue the bg base is --line-deep
   (not the hue) while border/dot/text use --color-grey-7. These differ per SoT. */
.pill--offline {
  --pill-bg: color-mix(in srgb, var(--line-deep) 18%, transparent);
  --pill-border: var(--color-grey-7);
  --pill-dot: var(--color-grey-7);
  --pill-text: var(--color-grey-7);
}

/* On-air tally pulse. Code-side runtime affordance with no Figma counterpart to
   contradict — same justification MenuList.vue records for its :hover state
   ("Figma models only active|Normal, so hover is a code-side runtime affordance").
   Motion-safe: disabled under prefers-reduced-motion below. */
.pill--on-air .pill__dot {
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 60%, transparent);
  animation: tally-pulse 2s ease-out infinite;
}
@keyframes tally-pulse {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 55%, transparent); }
  70%  { box-shadow: 0 0 0 6px color-mix(in srgb, var(--red) 0%, transparent); }
  100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--red) 0%, transparent); }
}

/* ==========================================================================
 * 7. PAGE HEADER + TOOLBAR
 * domain-tvu.md M5: monitoring consoles expose NO create entry — there is
 * deliberately no "+ New source" button here. Sources are created by the
 * backend / MicroApp and pushed to this console.
 * ========================================================================== */
.page__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-l);
  flex-wrap: wrap;
  margin-bottom: var(--sp-m);
}
.page__title-wrap { display: grid; gap: var(--sp-xs); }
.page__title { margin: 0; font: var(--text-style-large-title-24); color: var(--text-heading); }
.page__summary { display: flex; align-items: center; gap: var(--sp-xs); flex-wrap: wrap; }
.page__meta {
  font: var(--text-style-tips);
  color: var(--text-tips);
  font-variant-numeric: tabular-nums;
}

.toolbar {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  flex-wrap: wrap;
}
.field { position: relative; display: inline-flex; align-items: center; }
.field__icon {
  position: absolute;
  left: var(--sp-s);
  color: var(--icon-placeholder);
  pointer-events: none;
}
.field__icon--trailing { left: auto; right: var(--sp-s); }
.field input {
  height: var(--control-height-xxs);
  padding-block: 0;
  padding-left: var(--sp-xl);
  width: 260px;
}
.field select {
  height: var(--control-height-xxs);
  padding-block: 0;
  padding-right: var(--sp-xl);
  -webkit-appearance: none;
  appearance: none;
  cursor: pointer;
}
.field input:focus-visible,
.field select:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }

/* ==========================================================================
 * 8. TABLE — 1:1 mirror of src/components/Table/Table.vue
 * CANONICAL-029: Figma cell auto-layout is L0 / R8 / T8 / B8 — no left padding,
 * content hugs the left grid line (owner-confirmed Figma-as-SoT 2026-06-03).
 * INFRA-F129 ①: `.tbl td { border-top: 0 }` neutralises the global element
 * baseline, which otherwise wins the border-collapse conflict on the Vue
 * consumption chain while the shadow-DOM CE chain shows --line-light. Figma
 * tbody cells were live-verified strokes: [] — the row rule below is the truth.
 * ========================================================================== */
.panel {
  background: var(--bg-layer2);
  border: 1px solid var(--line-deep);
  border-radius: var(--r-m);
  padding: var(--sp-xs) var(--sp-m) 0;
}
.tbl-wrap { position: relative; overflow-x: auto; }
.tbl { width: 100%; min-width: 1080px; border-collapse: collapse; font: var(--text-style-body); color: var(--text-body); background: var(--bg-layer2); }
.tbl td { border-top: 0; }
.tbl-head .tbl-cell {
  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;
  text-align: left;
}
.tbl-body .tbl-row { border-bottom: 1px solid var(--line-light); transition: background .12s; }
.tbl-body .tbl-row:last-child { border-bottom: none; }
.tbl-body .tbl-row:hover { background: var(--bg-layer3); }
.tbl-body .tbl-cell {
  padding: var(--sp-xs) var(--sp-xs) var(--sp-xs) 0;
  font: var(--text-style-body);
  color: var(--text-body);
  vertical-align: middle;
}
.tbl-cell__content { display: flex; align-items: center; gap: var(--sp-xxs); }
.tbl-cell--right .tbl-cell__content { justify-content: flex-end; }
.tbl-empty { text-align: center; padding: var(--sp-m) var(--sp-xs); color: var(--text-tips); }

.panel__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-m);
  padding: var(--sp-s) 0;
  border-top: 1px solid var(--line-deep);
  font: var(--text-style-tips);
  color: var(--text-tips);
  font-variant-numeric: tabular-nums;
}

/* ---- cell internals ---- */
.cell { display: grid; gap: 2px; min-width: 0; }
.cell__primary {
  display: flex;
  align-items: center;
  gap: var(--sp-xxs);
  min-width: 0;
  color: var(--text-body);
}
/* M7: identifiers are SECONDARY — mid-level type weight, operators only copy
   them occasionally. Actions outrank them, status pills are the quietest. */
.cell__secondary {
  display: flex;
  align-items: center;
  gap: var(--sp-xxs);
  min-width: 0;
  font: var(--text-style-tips);
  color: var(--text-tips);
  font-variant-numeric: tabular-nums;
}
.cell__icon { color: var(--icon-default); }

/* R23 / M43 — single-line tail ellipsis. Width is constrained by the parent
   column; nowrap alone would blow out the layout. */
.truncate {
  display: block;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.mono-ish { font-variant-numeric: tabular-nums; letter-spacing: .01em; }

/* M29 Form 1 · Copyable: TVU icon/Edit/Copy immediately after the text.
   Figma spec gap = 4px, icon 12×12 for table row heights, fill Icon/Default. */
.copy-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--icon-default);
  cursor: pointer;
  flex: 0 0 auto;
  border-radius: var(--r-xs);
}
.copy-btn .icon { width: 12px; height: 12px; }
.copy-btn:hover { color: var(--icon-active); }
.copy-btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.copy-btn.is-copied { color: var(--brand); }

/* M29 Form 2 · Empty placeholder: em dash, Placeholder token, and NO copy icon.
   The visual contract is "intentionally empty, not copyable". */
.empty-value { color: var(--text-placeholder); }

/* M11 — trend needs TWO layers: the ↑↓ icon AND the value itself coloured. */
.metric { display: flex; align-items: baseline; gap: var(--sp-xxs); font-variant-numeric: tabular-nums; }
.metric__trend { width: 12px; height: 12px; align-self: center; }
.is-degraded { color: var(--orange); }
.is-healthy  { color: var(--text-body); }

.row-actions { display: flex; align-items: center; gap: var(--sp-xs); justify-content: flex-end; }

/* ==========================================================================
 * 9. ROW OVERFLOW MENU — mirror of MenuList.vue .menu-list--vertical.
 * The panel chrome (surface / border / radius / elevation) is composed from
 * tokens: MenuList has no Figma component-set of its own (registered divergence
 * logo-menulist-usermenu-code-first-2026-07-02), so only the item metrics below
 * are 1:1 from the SFC.
 * ========================================================================== */
.menu-pop {
  position: absolute;
  right: 0;
  z-index: var(--z-dropdown);
  min-width: 180px;
  padding: var(--sp-xxs);
  background: var(--bg-layer3);
  border: 1px solid var(--line-border);
  border-radius: var(--r-s);
  box-shadow: var(--shadow-l1);
}
.menu-pop[hidden] { display: none; }
.menu-list--vertical { flex-direction: column; gap: var(--sp-xxs); min-width: 180px; }
.menu-list--vertical .menu-list__item {
  display: flex;
  align-items: center;
  gap: var(--sp-xs);
  padding: var(--sp-xs) var(--sp-s);
  border-radius: var(--r-s);
  font: var(--text-style-body);
  color: var(--text-2);
  width: 100%;
  text-align: left;
}
.menu-list--vertical .menu-list__item:hover:not(.is-disabled) {
  background: var(--bg-layer3);
  color: var(--text-heading);
}
.menu-pop .menu-list__item:hover:not(.is-disabled) { background: var(--bg-layer4); }
.menu-list--vertical .menu-list__item--danger { color: var(--red); }
.action-wrap { position: relative; display: inline-flex; }

/* ==========================================================================
 * 10. CONFIRM DIALOG
 * Scrim + focus behaviour: 1:1 from src/components/PopupBox/PopupBox.vue
 * (--mask-overlay + backdrop-filter, z-modal, centred flex, closeOnBackdrop
 * defaults to false — correct for a destructive confirm).
 * ⚠️ Value conflict, reported not resolved: mockup-conventions.md M41 specifies
 * scrim rgba(0,0,0,0.2) + blur 8 (user-validated 2026-05-26), while
 * --mask-overlay is rgba(0,0,0,0.6) + blur 8 and is annotated "figma 真源:
 * Mask blur8 #000-60%". This page follows the TOKEN, because it is the code-side
 * source of truth the shipped PopupBox uses and M41's own code mirror (R21) is
 * still marked "候选、待补". Needs an owner ruling.
 *
 * Dialog itself: 1:1 from src/components/Notification/Notification.vue with
 * form="pop confirm" type="danger" — which per that component means
 * (a) no category icon, (b) Cancel ghost/gray + Confirm filling/RED,
 * (c) description painted --text-body (not Tips grey), (d) width 480.
 * M41: centred single dialog, ≤720×320, never a multi-variant artifact.
 * ========================================================================== */
.popup-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--mask-overlay);
  backdrop-filter: blur(var(--mask-overlay-blur));
  padding: var(--sp-m);
}
.popup-overlay[hidden] { display: none; }

.notif {
  display: grid;
  gap: var(--sp-xl);
  width: var(--notification-width-default);
  max-width: 100%;
  padding: var(--sp-m) var(--sp-l);
  border-radius: 6px;
  border: 1.2px solid var(--line-light);
  box-shadow: var(--shadow-l3);
  background: var(--notification-bg-dark);
  color: var(--text-body);
  outline: none;
}
.notif__main { display: flex; align-items: flex-start; gap: var(--sp-m); }
.notif__body { flex: 1 1 auto; min-width: 0; display: grid; gap: var(--sp-s); }
.notif__header { display: flex; align-items: center; gap: var(--sp-m); }
.notif__title { flex: 1 1 auto; min-width: 0; margin: 0; font: var(--text-style-pop-title); color: var(--text-body); }
.notif__desc { margin: 0; font: var(--text-style-body); color: var(--text-tips); }
.notif--pop-confirm .notif__desc { color: var(--text-body); }
.notif__close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--icon-default);
  cursor: pointer;
}
.notif__close:hover { color: var(--icon-active); }
.notif__close .icon { width: 12px; height: 12px; }
.notif__actions { display: flex; justify-content: flex-end; gap: var(--sp-m); }
.notif__icon { flex: 0 0 auto; width: 24px; height: 24px; }
.notif__icon .icon { width: 24px; height: 24px; }

/* form="slide" — passive toast, narrower, tighter gap, no action row. */
.toast-region {
  position: fixed;
  right: var(--sp-l);
  bottom: var(--sp-l);
  z-index: var(--z-select-portal);
  display: grid;
  gap: var(--sp-s);
}
.notif--slide { width: var(--notification-width-side-pop); gap: var(--sp-s); }

/* ==========================================================================
 * 11. TOOLTIP — R23 requires the TVU Tooltip, not a hand-rolled div. No build
 * chain here, so the visual is mirrored from the DS tooltip tokens
 * (--tooltip-bg-dark / --tooltip-border-dark / --tooltip-text-dark /
 * --tooltip-max-width). The native title attribute stays as the no-JS + touch +
 * screen-reader fallback (R23 "a11y 双保险").
 * ========================================================================== */
.tip {
  position: fixed;
  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);
  box-shadow: var(--shadow-l1);
  pointer-events: none;
  word-break: break-all;
}
.tip[hidden] { display: none; }

/* ==========================================================================
 * 12. RESPONSIVE + MOTION
 * Breakpoint literals must be members of the --bp-* token set
 * (640 / 960 / 1100 / 1280) — CSS @media cannot take var(), so
 * `pnpm audit:layout-tokens` checks the literal instead.
 * ========================================================================== */
@media (max-width: 1100px) {
  .topbar { gap: var(--sp-l); }
  .topbar-menu { gap: var(--sp-m); }
  .page { padding-inline: var(--sp-m); }
}
@media (max-width: 640px) {
  .field input { width: 100%; }
  .page__head { align-items: flex-start; }
}
@media (prefers-reduced-motion: reduce) {
  .pill--on-air .pill__dot { animation: none; }
  * { transition-duration: 0s !important; }
}
</style>
</head>

<body>

<!-- ==========================================================================
   ICON SPRITE
   Every glyph below is copied verbatim from tvu-design-system
   dist/icons/svg/<namespace>/<name>__<figmaNodeId>.svg — R15 §icons / R1:
   icons come from the TVU library, never hand-drawn. The file's own node id is
   kept in the symbol id comment so each one is traceable back to Figma.
   The two Message icons keep their baked fills on purpose: Notification.vue
   documents them as "baked-color message catalog icons (exact Figma fidelity)".
   The logo keeps #56AF31 because no token carries that value (a brand asset,
   not a themed surface) — audit:no-hardcoded-design-tokens treats a literal
   with no matching token as report-only, not blocking.
   ========================================================================== -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
  <!-- logo/TVU__1136_3266 -->
  <symbol id="i-logo" 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>
  <!-- Menu/More__1150_23671 -->
  <symbol id="i-more" viewBox="0 0 16 16"><path d="M9.5 3.5C9.5 4.32843 8.82843 5 8 5C7.17157 5 6.5 4.32843 6.5 3.5C6.5 2.67157 7.17157 2 8 2C8.82843 2 9.5 2.67157 9.5 3.5ZM9.5 8C9.5 8.82843 8.82843 9.5 8 9.5C7.17157 9.5 6.5 8.82843 6.5 8C6.5 7.17157 7.17157 6.5 8 6.5C8.82843 6.5 9.5 7.17157 9.5 8ZM8 14C8.82843 14 9.5 13.3284 9.5 12.5C9.5 11.6716 8.82843 11 8 11C7.17157 11 6.5 11.6716 6.5 12.5C6.5 13.3284 7.17157 14 8 14Z" fill="currentColor"/></symbol>
  <!-- Message/warning-1__1517_27750 (baked #F68512 = --orange dark, per Notification.vue) -->
  <symbol id="i-warning" viewBox="0 0 16 16"><path d="M8 14.9991C6.14283 15.0015 4.36136 14.2634 3.05022 12.9482C1.73569 11.6374 0.997842 9.85676 1.00001 8.00044C0.99751 6.14337 1.73573 4.36197 3.05109 3.05097C4.36181 1.7362 6.14256 0.998074 7.99912 1.00001C9.85629 0.997607 11.6378 1.73576 12.9489 3.05097C14.264 4.36182 15.0022 6.14282 15 7.99956C15.0024 9.85661 14.2642 11.638 12.9489 12.949C11.6378 14.2642 9.85629 15.0024 7.99912 15L8 14.9991ZM8 2.00002C6.39755 2.00002 4.89143 2.62436 3.75746 3.75746C2.62854 4.88017 1.9957 6.40786 2.00002 8C2.00002 9.60245 2.62436 11.1094 3.75746 12.2425C4.88017 13.3715 6.40786 14.0043 8 14C9.60245 14 11.1094 13.3756 12.2425 12.2425C13.3715 11.1198 14.0043 9.59214 14 8C14 6.39755 13.3756 4.89143 12.2425 3.75746C11.1198 2.62854 9.59214 1.9957 8 2.00002ZM7.3999 4.6C7.3999 4.26863 7.66853 4 7.9999 4C8.33127 4 8.5999 4.26863 8.5999 4.6V9C8.5999 9.33137 8.33127 9.6 7.9999 9.6C7.66853 9.6 7.3999 9.33137 7.3999 9V4.6ZM8.5999 11.4C8.5999 11.7314 8.33127 12 7.9999 12C7.66853 12 7.3999 11.7314 7.3999 11.4C7.3999 11.0686 7.66853 10.8 7.9999 10.8C8.33127 10.8 8.5999 11.0686 8.5999 11.4Z" fill="#F68512"/></symbol>
  <!-- Message/Success-2__1168_20881 (baked #2FB54E, per Notification.vue) -->
  <symbol id="i-success" viewBox="0 0 16 16"><path d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM5.22424 7.37564L7.03029 9.18159C7.10839 9.2597 7.23502 9.25969 7.31313 9.18159L10.6017 5.89303C10.836 5.65871 11.2159 5.65871 11.4502 5.89303C11.6846 6.12735 11.6846 6.50725 11.4502 6.74156L7.80811 10.3837C7.45664 10.7351 6.88681 10.7351 6.53533 10.3837L4.37573 8.22419C4.14141 7.98988 4.1414 7.60998 4.37571 7.37566C4.61002 7.14134 4.98991 7.14133 5.22424 7.37564Z" fill="#2FB54E"/></symbol>
  <!-- input/receiver__2343_16167 -->
  <symbol id="i-receiver" viewBox="0 0 16 16"><path d="M11.0061 3.90039C11.1066 3.90039 11.2056 3.92568 11.2939 3.97393L14.6878 5.82933C14.8803 5.93456 15 6.13643 15 6.3558V11.4C15 11.7314 14.7314 12 14.4 12H1.6C1.26863 12 1 11.7314 1 11.4V6.3558C1 6.13643 1.11971 5.93456 1.31219 5.82933L4.70617 3.97392C4.79442 3.92568 4.89339 3.90039 4.99397 3.90039H11.0061ZM12.3499 5.90589L10.8848 5.12395C10.8558 5.10848 10.8235 5.10039 10.7906 5.10039H5.20938C5.17654 5.10039 5.14419 5.10848 5.11522 5.12395L3.65007 5.90589C3.60415 5.93039 3.62157 6 3.67361 6H12.3264C12.3785 6 12.3959 5.93039 12.3499 5.90589ZM2.4 7.2C2.28954 7.2 2.2 7.28954 2.2 7.4V10.6C2.2 10.7105 2.28954 10.8 2.4 10.8H13.6C13.7104 10.8 13.8 10.7105 13.8 10.6V7.4C13.8 7.28954 13.7104 7.2 13.6 7.2H2.4ZM6.14999 9.39922C6.61943 9.39922 6.99999 9.01866 6.99999 8.54922C6.99999 8.07978 6.61943 7.69922 6.14999 7.69922C5.68055 7.69922 5.29999 8.07978 5.29999 8.54922C5.29999 9.01866 5.68055 9.39922 6.14999 9.39922ZM10.7 8.54922C10.7 9.01866 10.3194 9.39922 9.85 9.39922C9.38056 9.39922 9 9.01866 9 8.54922C9 8.07978 9.38056 7.69922 9.85 7.69922C10.3194 7.69922 10.7 8.07978 10.7 8.54922ZM11.4 10C11.4552 10 11.5 10.0448 11.5 10.1V10.3C11.5 10.3552 11.4552 10.4 11.4 10.4H10.6C10.5448 10.4 10.5 10.3552 10.5 10.3V10.1C10.5 10.0448 10.5448 10 10.6 10H11.4ZM12.9 10C12.9552 10 13 10.0448 13 10.1V10.3C13 10.3552 12.9552 10.4 12.9 10.4H12.1C12.0448 10.4 12 10.3552 12 10.3V10.1C12 10.0448 12.0448 10 12.1 10H12.9Z" fill="currentColor"/></symbol>
  <!-- Search/Search__1249_24010 -->
  <symbol id="i-search" viewBox="0 0 16 16"><path d="M11.6529 12.5025C10.5267 13.4389 9.07914 14.002 7.50006 14.002C3.91021 14.002 1.00006 11.0918 1.00006 7.50195C1.00006 3.9121 3.91021 1.00195 7.50006 1.00195C11.0899 1.00195 14.0001 3.9121 14.0001 7.50195C14.0001 9.08062 13.4373 10.5278 12.5014 11.6539L14.8227 13.9753C15.057 14.2096 15.057 14.5895 14.8227 14.8238C14.5884 15.0581 14.2085 15.0581 13.9742 14.8238L11.6529 12.5025ZM12.8001 7.50117C12.8001 4.57406 10.4272 2.20117 7.50007 2.20117C4.57296 2.20117 2.20007 4.57406 2.20007 7.50117C2.20007 10.4283 4.57296 12.8012 7.50007 12.8012C10.4272 12.8012 12.8001 10.4283 12.8001 7.50117Z" fill="currentColor"/></symbol>
  <!-- Edit/Copy__1155_25119 -->
  <symbol id="i-copy" viewBox="0 0 16 16"><path d="M2.2 1C1.53726 1 1 1.53726 1 2.2V11.4C1 12.0627 1.53726 12.6 2.2 12.6H3.40002V13.8004C3.40002 14.4631 3.93728 15.0004 4.60002 15.0004H13.8C14.4628 15.0004 15 14.4631 15 13.8004V4.60039C15 3.93765 14.4628 3.40039 13.8 3.40039H12.6V2.2C12.6 1.53726 12.0627 1 11.4 1H2.2ZM11.4 3.40039H4.60002C3.93728 3.40039 3.40002 3.93765 3.40002 4.60039V11.3992H2.60001C2.3791 11.3992 2.20001 11.2201 2.20001 10.9992V2.59922C2.20001 2.3783 2.3791 2.19922 2.60001 2.19922H11C11.2209 2.19922 11.4 2.3783 11.4 2.59922V3.40039ZM4.60004 4.99961C4.60004 4.77869 4.77912 4.59961 5.00004 4.59961H13.4C13.621 4.59961 13.8 4.7787 13.8 4.99961V13.3996C13.8 13.6205 13.621 13.7996 13.4 13.7996H5.00004C4.77912 13.7996 4.60004 13.6205 4.60004 13.3996V4.99961Z" fill="currentColor"/></symbol>
  <!-- Edit/Close__1155_24553 (registry name action/close) -->
  <symbol id="i-close" viewBox="0 0 16 16"><path d="M12.5758 13.4247C12.8101 13.659 13.19 13.659 13.4243 13.4247C13.6586 13.1903 13.6586 12.8104 13.4243 12.5761L8.84898 8.00082L13.4248 3.42543C13.6591 3.19113 13.6591 2.81123 13.4248 2.5769C13.1905 2.34258 12.8106 2.34256 12.5763 2.57687L8.00045 7.15229L3.42429 2.57613C3.18997 2.34181 2.81007 2.34181 2.57576 2.57613C2.34145 2.81044 2.34145 3.19034 2.57576 3.42465L7.15188 8.00078L2.57596 12.5763C2.34163 12.8106 2.34161 13.1905 2.57592 13.4248C2.81022 13.6592 3.19012 13.6592 3.42445 13.4249L8.00041 8.84931L12.5758 13.4247Z" fill="currentColor"/></symbol>
  <!-- load/refresh__1202_4182 -->
  <symbol id="i-refresh" viewBox="0 0 16 16"><path d="M10.8367 1.60055C9.46757 0.993637 7.93986 0.84184 6.47805 1.16746C5.01623 1.49308 3.69739 2.27896 2.71528 3.40962C1.73317 4.54028 1.13958 5.95612 1.02172 7.44912C0.903855 8.94211 1.26794 10.4336 2.06047 11.7043C2.853 12.9751 4.0322 13.9581 5.42481 14.5091C6.81743 15.0601 8.35004 15.1499 9.79746 14.7653C11.2449 14.3807 12.5308 13.5421 13.4663 12.3726C13.6733 12.1138 13.6313 11.7362 13.3726 11.5292C13.1138 11.3222 12.7362 11.3642 12.5292 11.623C11.7541 12.592 10.6886 13.2869 9.48933 13.6055C8.29003 13.9242 7.02016 13.8498 5.86628 13.3933C4.71239 12.9367 3.73534 12.1222 3.07867 11.0693C2.422 10.0164 2.12034 8.78061 2.21799 7.54355C2.31565 6.3065 2.80748 5.13338 3.62123 4.19654C4.43498 3.25971 5.52773 2.60856 6.73895 2.33876C7.95017 2.06895 9.21599 2.19473 10.3504 2.6976C11.3659 3.14773 12.2282 3.87877 12.8379 4.80078H11.6C11.2686 4.80078 11 5.06941 11 5.40078C11 5.73215 11.2686 6.00078 11.6 6.00078H14.4C14.7314 6.00078 15 5.73215 15 5.40078L15 5.39692V2.6C15 2.26863 14.7314 2 14.4 2C14.0686 2 13.8 2.26863 13.8 2.6V4.08078C13.0666 2.99539 12.0409 2.13433 10.8367 1.60055Z" fill="currentColor"/></symbol>
  <!-- Setting/Setting-1__1228_3993 -->
  <symbol id="i-setting" viewBox="0 0 16 16"><path d="M1.76916 5.96452C1.49393 6.01957 1.23163 6.18593 1.17012 6.45978C1.05876 6.95553 1 7.47117 1 8.00053C1 8.52951 1.05868 9.04478 1.16987 9.5402C1.23138 9.81422 1.4939 9.98063 1.7693 10.0356C2.2596 10.1335 2.70691 10.4339 2.97654 10.9009C3.24594 11.3676 3.28268 11.9047 3.12274 12.3779C3.03292 12.6437 3.04591 12.9538 3.252 13.1442C4.00615 13.8407 4.9138 14.3734 5.91848 14.6858C6.18642 14.7692 6.46145 14.625 6.64651 14.4141C6.97641 14.0381 7.46051 13.8007 8.00004 13.8007C8.53957 13.8007 9.02365 14.0381 9.35356 14.4141C9.53862 14.625 9.81365 14.7692 10.0816 14.6858C11.0866 14.3732 11.9945 13.8403 12.7487 13.1435C12.9547 12.9532 12.9677 12.6432 12.878 12.3775C12.7182 11.9043 12.755 11.3674 13.0243 10.9009C13.2938 10.4341 13.7409 10.1336 14.231 10.0356C14.5063 9.98061 14.7686 9.8142 14.8301 9.54028C14.9413 9.04484 15 8.52954 15 8.00053C15 7.47125 14.9413 6.9557 14.8299 6.46003C14.7684 6.18606 14.5059 6.01968 14.2306 5.96471C13.7403 5.86684 13.2931 5.56637 13.0235 5.09938C12.7541 4.63289 12.7173 4.09595 12.8771 3.62279C12.9669 3.35704 12.9538 3.04697 12.7478 2.85668C11.9935 2.16013 11.0857 1.62741 10.0808 1.31499C9.813 1.23173 9.5381 1.37575 9.35305 1.58648C9.02316 1.96218 8.53927 2.19932 8.00001 2.19932C7.46074 2.19932 6.97685 1.96218 6.64696 1.58648C6.46191 1.37575 6.18701 1.23173 5.91921 1.31499C4.91404 1.6275 4.00598 2.16045 3.25156 2.8573C3.0456 3.04755 3.03252 3.35748 3.12217 3.62314C3.28179 4.0962 3.24494 4.63296 2.97569 5.09931C2.70619 5.5661 2.25919 5.86652 1.76916 5.96452ZM10.1807 2.62328C10.6807 2.82632 11.1461 3.0971 11.5653 3.42423C11.6258 3.47146 11.6515 3.54987 11.6368 3.6252C11.5028 4.31099 11.6065 5.04638 11.9833 5.69903C12.3603 6.35195 12.9456 6.80956 13.6069 7.03637C13.6796 7.06132 13.7347 7.12297 13.7453 7.19916C13.7814 7.46074 13.8 7.72788 13.8 7.99939C13.8 8.2709 13.7814 8.53805 13.7453 8.79964C13.7347 8.87583 13.6796 8.93747 13.6069 8.96243C12.9457 9.18924 12.3603 9.64684 11.9834 10.2998C11.6066 10.9524 11.5029 11.6878 11.6368 12.3736C11.6515 12.4489 11.6258 12.5273 11.5653 12.5745C11.146 12.9017 10.6805 13.1726 10.1804 13.3756C10.1091 13.4045 10.0283 13.3876 9.97032 13.3371C9.44325 12.8776 8.75411 12.5994 7.99999 12.5994C7.24588 12.5994 6.55675 12.8776 6.02967 13.3371C5.97174 13.3876 5.89085 13.4045 5.81964 13.3756C5.31937 13.1725 4.85385 12.9016 4.43449 12.5743C4.37397 12.5271 4.34827 12.4487 4.36301 12.3733C4.49719 11.6873 4.39356 10.9516 4.01659 10.2986C3.63962 9.6457 3.05426 9.18807 2.39302 8.96128C2.32025 8.93632 2.26511 8.87465 2.25461 8.79845C2.21861 8.53724 2.20001 8.27049 2.20001 7.99939C2.20001 7.7283 2.21861 7.46155 2.2546 7.20035C2.26511 7.12414 2.32025 7.06247 2.39302 7.03752C3.05427 6.81072 3.63965 6.3531 4.01663 5.70014C4.3936 5.04721 4.49724 4.31148 4.36304 3.62544C4.3483 3.55009 4.37401 3.47166 4.43453 3.42442C4.85379 3.09721 5.31918 2.82637 5.81933 2.6233C5.89055 2.59438 5.97145 2.61134 6.02939 2.66185C6.5565 3.12143 7.24574 3.39978 7.99999 3.39978C8.75425 3.39978 9.4435 3.12143 9.97061 2.66184C10.0285 2.61133 10.1094 2.59437 10.1807 2.62328ZM10.5 8C10.5 9.38071 9.38071 10.5 8 10.5C6.61929 10.5 5.5 9.38071 5.5 8C5.5 6.61929 6.61929 5.5 8 5.5C9.38071 5.5 10.5 6.61929 10.5 8ZM9.29995 7.99922C9.29995 7.28125 8.71792 6.69922 7.99995 6.69922C7.28198 6.69922 6.69995 7.28125 6.69995 7.99922C6.69995 8.71719 7.28198 9.29922 7.99995 9.29922C8.71792 9.29922 9.29995 8.71719 9.29995 7.99922Z" fill="currentColor"/></symbol>
  <!-- Arrow/Dropdown__1150_23504 -->
  <symbol id="i-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/Down__1149_23407 -->
  <symbol id="i-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>
</svg>

<!-- ==========================================================================
   TOP BAR + page-level navigation
   ========================================================================== -->
<header class="topbar" data-figma-component="Top bar" data-figma-tag="After Login">
  <div class="topbar-brand">
    <span class="logo" role="img" aria-label="TVU logo"><svg viewBox="0 0 16 16"><use href="#i-logo"/></svg></span>
  </div>

  <nav class="topbar-menu" aria-label="Primary">
    <ul class="menu-list menu-list--horizontal" role="menubar" id="nav"></ul>
  </nav>

  <div class="topbar-right">
    <button class="btn btn--text-gray btn--icon" type="button" id="refreshTop" aria-label="Refresh now">
      <svg class="icon"><use href="#i-refresh"/></svg>
    </button>
    <button class="btn btn--text-gray btn--icon" type="button" aria-label="Settings">
      <svg class="icon"><use href="#i-setting"/></svg>
    </button>
    <span class="avatar" role="img" aria-label="Signed in as Dana Okoye">DO</span>
  </div>
</header>

<main class="page">

  <!-- ---- page header: title + status roll-up + toolbar ------------------ -->
  <div class="page__head">
    <div class="page__title-wrap">
      <h1 class="page__title" id="pageTitle"></h1>
      <div class="page__summary" id="summary"></div>
    </div>

    <div class="toolbar">
      <span class="field">
        <svg class="icon field__icon"><use href="#i-search"/></svg>
        <label class="sr-only" for="q" id="lblSearch"></label>
        <input id="q" type="search" autocomplete="off">
      </span>

      <span class="field">
        <label class="sr-only" for="stateFilter" id="lblFilter"></label>
        <select id="stateFilter"></select>
        <svg class="icon field__icon field__icon--trailing"><use href="#i-dropdown"/></svg>
      </span>

      <button class="btn btn--ghost-gray" type="button" id="refresh"
              data-figma-component="Button" data-figma-style="ghost" data-figma-color="gray 1" data-figma-size="M">
        <span id="lblRefresh"></span>
      </button>
    </div>
  </div>

  <p class="page__meta" id="freshness"></p>

  <!-- ---- table -------------------------------------------------------- -->
  <section class="panel" aria-labelledby="pageTitle">
    <div class="tbl-wrap">
      <table class="tbl">
        <thead class="tbl-head">
          <tr id="thead"></tr>
        </thead>
        <tbody class="tbl-body" id="tbody"></tbody>
      </table>
    </div>
    <div class="panel__foot">
      <span id="rowCount"></span>
      <span id="footNote"></span>
    </div>
  </section>
</main>

<!-- ==========================================================================
   DESTRUCTIVE CONFIRM — one dialog, full-viewport scrim (M41)
   ========================================================================== -->
<div class="popup-overlay" id="scrim" hidden>
  <div class="notif notif--dark notif--pop-confirm notif--type-danger"
       id="dialog" role="dialog" aria-modal="true" aria-labelledby="dlgTitle" aria-describedby="dlgDesc" tabindex="-1"
       data-figma-component="Notification" data-figma-form="pop confirm" data-figma-type="danger" data-figma-theme="dark">
    <div class="notif__main">
      <!-- form="pop confirm" carries NO category icon (Notification.vue showsIcon) -->
      <div class="notif__body">
        <div class="notif__header">
          <p class="notif__title" id="dlgTitle"></p>
          <button class="notif__close" type="button" id="dlgClose" aria-label="Close">
            <svg class="icon"><use href="#i-close"/></svg>
          </button>
        </div>
        <p class="notif__desc" id="dlgDesc"></p>
      </div>
    </div>
    <div class="notif__actions">
      <button class="btn btn--ghost-gray" type="button" id="dlgCancel"
              data-figma-style="ghost" data-figma-color="gray 1" data-figma-size="M"></button>
      <button class="btn btn--fill-red" type="button" id="dlgConfirm"
              data-figma-style="filling" data-figma-color="red" data-figma-size="M"></button>
    </div>
  </div>
</div>

<div class="toast-region" id="toasts" role="status" aria-live="polite"></div>
<div class="tip" id="tip" role="tooltip" hidden></div>

<script>
/* ==========================================================================
 * PRODUCT UI STRINGS — single source, single language.
 * code-conventions.md R16 (code mirror of M33): product UI strings go through an
 * i18n key / const map, NEVER inline bilingual text. Even with no i18n framework
 * wired up, this map is the seam a real implementation swaps for vue-i18n (the
 * DS components already read from a locale contract, see useLocale()).
 * domain-tvu.md M9: inline action labels are LITERAL VERBS — never a glyph, an
 * abbreviation, or the object of the action standing in for the verb.
 * mockup-conventions.md M44: no backend service names or internal codenames.
 * ========================================================================== */
const T = {
  nav:            ['Dashboard', 'Live Sources', 'Receivers', 'Reports'],
  pageTitle:      'Live Sources',
  search:         'Search sources',
  searchLabel:    'Search sources by name, address or receiver',
  filterLabel:    'Filter by status',
  filterAll:      'All statuses',
  refresh:        'Refresh',
  freshness:      (t) => 'Auto-refresh every 5 s · last updated ' + t,
  columns:        ['Source', 'Source address', 'Target receiver', 'Status', 'Throughput', ''],
  actionsHeader:  'Actions',
  stop:           'Stop Stream',
  start:          'Start Stream',
  assign:         'Assign Receiver',
  details:        'Details',
  rename:         'Rename',
  copyUrl:        'Copy Address',
  remove:         'Remove',
  moreLabel:      (n) => 'More actions for ' + n,
  offlineNoStop:  'Source is offline — nothing to stop',
  onAir:          'On-air',
  warning:        'Warning',
  offline:        'Offline',
  neverStarted:   'Never started',
  stoppedJustNow: 'Stopped by operator · just now',
  startedJustNow: 'Starting · negotiating with receiver',
  rows:           (n, total) => 'Showing ' + n + ' of ' + total + ' sources',
  footNote:       'Sources are provisioned upstream and pushed to this console.',
  copied:         'Copied',
  empty:          'No sources match the current filter.',
  dlgTitle:       (n) => 'Stop “' + n + '”?',
  dlgDesc:        (n, r, up) =>
                    n + ' is on air to ' + r + ' and has been streaming for ' + up + '. ' +
                    'Stopping ends the stream immediately and the receiver output goes to black. ' +
                    'Restarting requires the source device to reconnect.',
  dlgCancel:      'Cancel',
  dlgConfirm:     'Stop Stream',
  toastStopped:   (n) => n + ' stopped. Receiver output released.',
};

const EM_DASH = '\u2014';   /* M29 Form 2 placeholder glyph */

/* ==========================================================================
 * DATA
 * State axis: 'on-air' | 'warning' | 'offline'.
 * domain-tvu.md M10 — "off / not running" is NEVER one flat Inactive: offline
 * rows carry sub = 'idle' (ran before, has history → "stopped, can restart") or
 * sub = 'never' (never configured → "configure first"), because the two user
 * mental models are different. Row 7 below is the NeverStarted bootstrap case
 * and gets the literal-verb CTA instead of a restart.
 * ========================================================================== */
const SOURCES = [
  { id:'SRC-1041', name:'NYC-Studio-A-Cam1', meta:'SRT · 1080p50',
    addr:'srt://ingest-us-east.tvu.example:9001?streamid=nyc-a1', region:'US-East ingest',
    rcv:'Studio A Rack', rcvId:'RCV-NY-01',
    state:'on-air', bitrate:12.4, loss:0.0, uptime:'02:14:38' },

  { id:'SRC-1042', name:'LON-OB-Van-2', meta:'Zixi · 1080i50',
    addr:'zixi://ingest-eu-west.tvu.example:2088/lon-ob2', region:'EU-West ingest',
    rcv:'Newsroom PCR', rcvId:'RCV-LDN-04',
    state:'on-air', bitrate:9.8, loss:0.1, uptime:'00:47:05' },

  { id:'SRC-1043', name:'TOK-Rooftop-Feed', meta:'RTMP · 720p50',
    addr:'rtmp://ingest-ap-northeast.tvu.example/live/tok-roof', region:'AP-Northeast ingest',
    rcv:'Master Control', rcvId:'RCV-TYO-02',
    state:'warning', bitrate:4.2, loss:4.2, uptime:'01:02:11',
    reason:'Packet loss 4.2% · bitrate under target 8 Mbps' },

  /* long address on purpose — exercises the R23 / M43 overflow contract */
  { id:'SRC-1044', name:'SFO-Presser-Hall-B', meta:'UDP multicast · 1080p30',
    addr:'udp://237.100.20.145:1234?pkt_size=1316&overrun_nonfatal=1&fifo_size=5000000&buffer_size=65536',
    region:'US-West ingest',
    rcv:'Pool Feed Decoder', rcvId:'RCV-SFO-11',
    state:'on-air', bitrate:15.1, loss:0.0, uptime:'00:12:44' },

  { id:'SRC-1045', name:'BER-Marathon-Bike-3', meta:'SRT · 1080p50 · bonded',
    addr:'srt://ingest-eu-central.tvu.example:9002?streamid=ber-bike3', region:'EU-Central ingest',
    rcv:'Sports Gallery', rcvId:'RCV-BER-07',
    state:'on-air', bitrate:11.6, loss:0.3, uptime:'03:31:57' },

  /* M10 Inactive-Idle — has history, "stopped, can restart" */
  { id:'SRC-1046', name:'SYD-Harbour-Cam', meta:'SRT · 1080p25',
    addr:'srt://ingest-ap-southeast.tvu.example:9004?streamid=syd-harbour', region:'AP-Southeast ingest',
    rcv:'Weather Wall', rcvId:'RCV-SYD-03',
    state:'offline', sub:'idle', reason:'Last ran 08:12 ' + '\u2192' + ' 09:47 · 95 min' },

  /* M10 Inactive-NeverStarted — no route, no history, CTA-only */
  { id:'SRC-1047', name:'DXB-Studio-C-Backup', meta:'Not configured',
    addr:null, region:null,
    rcv:null, rcvId:null,
    state:'offline', sub:'never', reason:T.neverStarted },
];

const STATE_META = {
  'on-air':  { label:T.onAir,   cls:'pill--on-air',  figma:'On-air'  },
  'warning': { label:T.warning, cls:'pill--warning', figma:'Warning' },
  'offline': { label:T.offline, cls:'pill--offline', figma:'Inactive'},
};

/* ==========================================================================
 * HELPERS
 * ========================================================================== */
const $ = (sel, root = document) => root.querySelector(sel);
const esc = (s) => String(s).replace(/[&<>"']/g, (c) =>
  ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c]));

/* R23 — center-ellipsis. CSS has no native support, so the string is split:
   head keeps the protocol + host, tail keeps the port / stream id. Both ends of
   a URL carry information, and M29's dev-side spec calls for exactly this so the
   Copy affordance is never covered. */
function centerEllipsis(text, head = 30, tail = 16) {
  if (text.length <= head + tail + 1) return text;
  return text.slice(0, head) + '...' + text.slice(-tail);
}

/* M29 Form 1 · Copyable — text hugs, then the TVU icon/Edit/Copy at 4px gap.
   M29 Form 2 · Empty — em dash in Placeholder colour and NO copy affordance:
   the visual contract is "intentionally empty, not copyable". */
function copyable(value, srLabel) {
  if (!value) return '<span class="empty-value">' + EM_DASH + '</span>';
  return '<button class="copy-btn" type="button" data-copy="' + esc(value) + '"'
       + ' aria-label="Copy ' + esc(srLabel) + '">'
       + '<svg class="icon"><use href="#i-copy"/></svg></button>';
}

function pill(state) {
  const m = STATE_META[state];
  return '<span class="pill ' + m.cls + '" data-figma-component="Pill Status"'
       + ' data-figma-status="' + m.figma + '" data-figma-active="true">'
       + '<span class="pill__dot" aria-hidden="true"></span>'
       + '<span class="pill__label">' + esc(m.label) + '</span></span>';
}

function summaryPill(state, count) {
  const m = STATE_META[state];
  return '<span class="pill ' + m.cls + '">'
       + '<span class="pill__dot" aria-hidden="true"></span>'
       + '<span class="pill__label">' + esc(m.label) + '</span>'
       + '<span class="pill__count">' + count + '</span></span>';
}

/* M11 — a trend needs TWO signals: the arrow glyph AND the value itself
   coloured. An arrow on its own is not enough. */
function throughput(s) {
  if (s.state === 'offline') {
    return '<span class="empty-value">' + EM_DASH + '</span>';
  }
  const degraded = s.state === 'warning';
  const tone = degraded ? 'is-degraded' : 'is-healthy';
  const arrow = degraded
    ? '<svg class="icon metric__trend is-degraded" aria-hidden="true"><use href="#i-down"/></svg>'
    : '';
  return '<span class="cell">'
       + '<span class="metric ' + tone + '">' + arrow
       + '<span>' + s.bitrate.toFixed(1) + ' Mbps</span></span>'
       + '<span class="cell__secondary ' + (degraded ? 'is-degraded' : '') + '">'
       + s.loss.toFixed(1) + '% loss</span></span>';
}

/* ==========================================================================
 * RENDER
 * ========================================================================== */
let filterState = 'all';
let query = '';

function visibleRows() {
  const q = query.trim().toLowerCase();
  return SOURCES.filter((s) => {
    if (filterState !== 'all' && s.state !== filterState) return false;
    if (!q) return true;
    return [s.name, s.addr, s.rcv, s.rcvId, s.id].some(
      (v) => v && String(v).toLowerCase().includes(q));
  });
}

function rowActions(s) {
  const offline = s.state === 'offline';

  /* The primary slot is always Stop Stream so the destructive control sits in
     the same place on every row. On an offline row it renders DISABLED (the
     canonical ghost/red disable palette, opacity .4) rather than disappearing —
     a control that moves between rows is worse than one that is visibly
     unavailable. M7: these stay full M-size (32px) buttons; the column widens
     rather than the buttons shrinking (Fitts's law). */
  const stop = '<button class="btn btn--ghost-red" type="button"'
    + (offline ? ' disabled title="' + esc(T.offlineNoStop) + '"' : ' data-stop="' + s.id + '"')
    + ' data-figma-component="Button" data-figma-style="ghost" data-figma-color="red"'
    + ' data-figma-size="M" data-figma-status="' + (offline ? 'disable' : 'default') + '">'
    + esc(T.stop) + '</button>';

  /* Secondary slot. M10: the NeverStarted sub-type gets a configure CTA, not a
     restart — the operator has nothing to restart yet. */
  let secondary;
  if (!offline) {
    secondary = '<button class="btn btn--text-gray" type="button" data-details="' + s.id + '">'
              + esc(T.details) + '</button>';
  } else if (s.sub === 'never') {
    secondary = '<button class="btn btn--ghost-green" type="button" data-assign="' + s.id + '">'
              + esc(T.assign) + '</button>';
  } else {
    secondary = '<button class="btn btn--ghost-green" type="button" data-start="' + s.id + '">'
              + esc(T.start) + '</button>';
  }

  const menuItems = [
    offline ? { label:T.details, act:'details' } : null,
    { label:T.rename, act:'rename' },
    s.addr ? { label:T.copyUrl, act:'copy' } : null,
    { label:T.remove, act:'remove', danger:true },
  ].filter(Boolean).map((it) =>
    '<li><button class="menu-list__item' + (it.danger ? ' menu-list__item--danger' : '') + '"'
    + ' role="menuitem" type="button" data-act="' + it.act + '" data-id="' + s.id + '">'
    + esc(it.label) + '</button></li>').join('');

  return '<span class="row-actions">' + stop + secondary
    + '<span class="action-wrap">'
    + '<button class="btn btn--text-gray btn--icon" type="button" data-menu="' + s.id + '"'
    + ' aria-haspopup="menu" aria-expanded="false" aria-label="' + esc(T.moreLabel(s.name)) + '">'
    + '<svg class="icon"><use href="#i-more"/></svg></button>'
    + '<div class="menu-pop" id="menu-' + s.id + '" hidden>'
    + '<ul class="menu-list menu-list--vertical" role="menu">' + menuItems + '</ul>'
    + '</div></span></span>';
}

function statusCell(s) {
  const reasonTone = s.state === 'warning' ? ' is-degraded' : '';
  const icon = s.state === 'warning'
    ? '<svg class="icon" style="width:12px;height:12px" aria-hidden="true"><use href="#i-warning"/></svg>'
    : '';
  const reason = s.reason
    ? '<span class="cell__secondary' + reasonTone + '">' + icon + '<span>' + esc(s.reason) + '</span></span>'
    : (s.uptime ? '<span class="cell__secondary">On air ' + esc(s.uptime) + '</span>' : '');
  return '<span class="cell">' + pill(s.state) + reason + '</span>';
}

function render() {
  const rows = visibleRows();

  $('#tbody').innerHTML = rows.length ? rows.map((s) => {
    const addrShort = s.addr ? centerEllipsis(s.addr) : null;
    return '<tr class="tbl-row" data-row="' + s.id + '">'
      + '<td class="tbl-cell"><span class="cell">'
        + '<span class="cell__primary"><span class="truncate" data-full="' + esc(s.name) + '">' + esc(s.name) + '</span></span>'
        + '<span class="cell__secondary">' + esc(s.meta) + ' · ' + esc(s.id) + '</span>'
      + '</span></td>'

      + '<td class="tbl-cell"><span class="cell">'
        + '<span class="cell__primary">'
          + (s.addr
              ? '<span class="truncate mono-ish" data-full="' + esc(s.addr) + '">' + esc(addrShort) + '</span>'
                + copyable(s.addr, 'source address of ' + s.name)
              : '<span class="empty-value">' + EM_DASH + '</span>')
        + '</span>'
        + (s.region ? '<span class="cell__secondary">' + esc(s.region) + '</span>' : '')
      + '</span></td>'

      + '<td class="tbl-cell"><span class="cell">'
        + '<span class="cell__primary">'
          + (s.rcv
              ? '<svg class="icon cell__icon" aria-hidden="true"><use href="#i-receiver"/></svg>'
                + '<span class="truncate" data-full="' + esc(s.rcv) + '">' + esc(s.rcv) + '</span>'
              : '<span class="empty-value">' + EM_DASH + '</span>')
        + '</span>'
        + (s.rcvId
            ? '<span class="cell__secondary"><span class="mono-ish">' + esc(s.rcvId) + '</span>'
              + copyable(s.rcvId, 'receiver id ' + s.rcvId) + '</span>'
            : '')
      + '</span></td>'

      + '<td class="tbl-cell">' + statusCell(s) + '</td>'
      + '<td class="tbl-cell">' + throughput(s) + '</td>'
      + '<td class="tbl-cell tbl-cell--right">' + rowActions(s) + '</td>'
      + '</tr>';
  }).join('') : '<tr><td class="tbl-empty" colspan="6">' + esc(T.empty) + '</td></tr>';

  const counts = { 'on-air':0, warning:0, offline:0 };
  SOURCES.forEach((s) => { counts[s.state]++; });
  $('#summary').innerHTML =
      summaryPill('on-air', counts['on-air'])
    + summaryPill('warning', counts.warning)
    + summaryPill('offline', counts.offline);

  $('#rowCount').textContent = T.rows(rows.length, SOURCES.length);
  detectTruncation();
}

/* ==========================================================================
 * R23 — truncation detection. Anti-pattern: hanging a tooltip on every clamped
 * container, so short values pop one too (visual noise + screen-reader noise).
 * A tooltip is attached ONLY where truncation actually happened: either the
 * layout clipped it (scrollWidth > clientWidth) or centerEllipsis rewrote the
 * string. The native title attribute is the no-JS / touch / screen-reader half
 * of the a11y pair.
 * ========================================================================== */
function detectTruncation() {
  document.querySelectorAll('.truncate[data-full]').forEach((el) => {
    const full = el.getAttribute('data-full');
    const clipped = el.scrollWidth > el.clientWidth;
    const rewritten = el.textContent !== full;
    if (clipped || rewritten) {
      el.setAttribute('title', full);
      el.dataset.truncated = 'true';
      el.tabIndex = 0;
    } else {
      el.removeAttribute('title');
      delete el.dataset.truncated;
      el.removeAttribute('tabindex');
    }
  });
}
if (window.ResizeObserver) {
  new ResizeObserver(() => detectTruncation()).observe(document.body);
}

/* Tooltip — mirrors the DS Tooltip tokens (see .tip). Hover AND keyboard focus,
   so it is reachable without a pointer. */
const tip = $('#tip');
function showTip(el) {
  const full = el.getAttribute('data-full');
  if (!full || !el.dataset.truncated) return;
  tip.textContent = full;
  tip.hidden = false;
  const r = el.getBoundingClientRect();
  const t = tip.getBoundingClientRect();
  tip.style.left = Math.max(8, Math.min(r.left, window.innerWidth - t.width - 8)) + 'px';
  tip.style.top  = (r.top - t.height - 8 < 8 ? r.bottom + 8 : r.top - t.height - 8) + 'px';
}
function hideTip() { tip.hidden = true; }
document.addEventListener('mouseover', (e) => {
  const el = e.target.closest('.truncate[data-truncated]');
  if (el) showTip(el); else hideTip();
});
document.addEventListener('focusin', (e) => {
  const el = e.target.closest('.truncate[data-truncated]');
  if (el) showTip(el); else hideTip();
});
document.addEventListener('scroll', hideTip, true);

/* ==========================================================================
 * CONFIRM DIALOG — behaviour mirrored from PopupBox.vue: body scroll lock,
 * focus moved to the first focusable, Tab trapped, Escape closes, focus
 * restored on close, and closeOnBackdrop stays FALSE (its default) so a stray
 * click outside cannot dismiss a destructive confirmation.
 * ========================================================================== */
const scrim = $('#scrim');
const dialog = $('#dialog');
let pendingId = null;
let lastFocused = null;
let savedOverflow = '';

const FOCUSABLE = 'button:not([disabled]),[href],input:not([disabled]),select:not([disabled]),[tabindex]:not([tabindex="-1"])';

function openConfirm(id) {
  const s = SOURCES.find((x) => x.id === id);
  if (!s) return;
  pendingId = id;
  $('#dlgTitle').textContent = T.dlgTitle(s.name);
  $('#dlgDesc').textContent  = T.dlgDesc(s.name, s.rcv + ' (' + s.rcvId + ')', s.uptime);
  lastFocused = document.activeElement;
  savedOverflow = document.body.style.overflow;
  document.body.style.overflow = 'hidden';
  scrim.hidden = false;
  requestAnimationFrame(() => {
    const f = dialog.querySelectorAll(FOCUSABLE);
    (f[f.length - 1] || dialog).focus();   /* land on Cancel-side, not on the destructive button */
  });
}

function closeConfirm() {
  scrim.hidden = true;
  pendingId = null;
  document.body.style.overflow = savedOverflow;
  if (lastFocused && lastFocused.focus) lastFocused.focus();
  lastFocused = null;
}

document.addEventListener('keydown', (e) => {
  if (scrim.hidden) {
    if (e.key === 'Escape') closeAllMenus();
    return;
  }
  if (e.key === 'Escape') { e.preventDefault(); closeConfirm(); return; }
  if (e.key !== 'Tab') return;
  const f = Array.from(dialog.querySelectorAll(FOCUSABLE));
  if (!f.length) { e.preventDefault(); dialog.focus(); return; }
  const first = f[0], last = f[f.length - 1];
  if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
  else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
});

$('#dlgCancel').addEventListener('click', closeConfirm);
$('#dlgClose').addEventListener('click', closeConfirm);
$('#dlgConfirm').addEventListener('click', () => {
  const s = SOURCES.find((x) => x.id === pendingId);
  if (s) {
    /* The state change IS the feedback: the row drops to the Inactive-Idle
       sub-type with its own history line, so the operator sees the tri-state
       transition rather than only a transient message. */
    s.state = 'offline';
    s.sub = 'idle';
    s.reason = T.stoppedJustNow;
    s.uptime = null;
    toast(T.toastStopped(s.name));
  }
  closeConfirm();
  render();
});

/* Notification form="slide" — passive toast, 320 wide, no action row. */
function toast(msg) {
  const el = document.createElement('div');
  el.className = 'notif notif--dark notif--slide';
  el.setAttribute('data-figma-component', 'Notification');
  el.setAttribute('data-figma-form', 'slide');
  el.setAttribute('data-figma-type', 'success');
  el.innerHTML = '<div class="notif__main">'
    + '<span class="notif__icon"><svg class="icon" aria-hidden="true"><use href="#i-success"/></svg></span>'
    + '<div class="notif__body"><div class="notif__header">'
    + '<p class="notif__title">' + esc(msg) + '</p></div></div></div>';
  $('#toasts').appendChild(el);
  setTimeout(() => el.remove(), 4000);
}

/* ==========================================================================
 * ROW MENU + ROW ACTIONS
 * ========================================================================== */
function closeAllMenus() {
  document.querySelectorAll('.menu-pop').forEach((m) => { m.hidden = true; });
  document.querySelectorAll('[data-menu]').forEach((b) => b.setAttribute('aria-expanded', 'false'));
}

document.addEventListener('click', (e) => {
  const menuBtn = e.target.closest('[data-menu]');
  const stopBtn = e.target.closest('[data-stop]');
  const startBtn = e.target.closest('[data-start]');
  const copyBtn = e.target.closest('[data-copy]');
  const item = e.target.closest('.menu-list__item[data-act]');

  if (!menuBtn && !item) closeAllMenus();

  if (menuBtn) {
    const pop = document.getElementById('menu-' + menuBtn.dataset.menu);
    const wasOpen = !pop.hidden;
    closeAllMenus();
    pop.hidden = wasOpen;
    menuBtn.setAttribute('aria-expanded', String(!wasOpen));
    if (!wasOpen) {
      const r = menuBtn.getBoundingClientRect();
      pop.style.top = (r.height + 4) + 'px';
      const first = pop.querySelector('.menu-list__item');
      if (first) first.focus();
    }
    return;
  }

  if (stopBtn) { openConfirm(stopBtn.dataset.stop); return; }

  if (startBtn) {
    /* Starting a stream is not destructive — no second confirmation. */
    const s = SOURCES.find((x) => x.id === startBtn.dataset.start);
    if (s) { s.state = 'on-air'; s.sub = null; s.reason = T.startedJustNow;
             s.bitrate = 0.0; s.loss = 0.0; s.uptime = '00:00:02'; render(); }
    return;
  }

  if (copyBtn) {
    const value = copyBtn.dataset.copy;
    if (navigator.clipboard) navigator.clipboard.writeText(value).catch(() => {});
    copyBtn.classList.add('is-copied');
    copyBtn.setAttribute('title', T.copied);
    setTimeout(() => { copyBtn.classList.remove('is-copied'); copyBtn.removeAttribute('title'); }, 1200);
    return;
  }

  if (item) {
    if (item.dataset.act === 'copy') {
      const s = SOURCES.find((x) => x.id === item.dataset.id);
      if (s && s.addr && navigator.clipboard) navigator.clipboard.writeText(s.addr).catch(() => {});
    }
    closeAllMenus();
  }
});

/* ==========================================================================
 * CHROME WIRE-UP
 * ========================================================================== */
$('#pageTitle').textContent = T.pageTitle;
$('#lblSearch').textContent = T.searchLabel;
$('#lblFilter').textContent = T.filterLabel;
$('#lblRefresh').textContent = T.refresh;
$('#footNote').textContent = T.footNote;
$('#q').placeholder = T.search;
$('#dlgCancel').textContent = T.dlgCancel;
$('#dlgConfirm').textContent = T.dlgConfirm;

$('#nav').innerHTML = T.nav.map((label, i) =>
  '<li class="menu-list__item' + (i === 1 ? ' is-active' : '') + '" role="menuitem"'
  + (i === 1 ? ' aria-current="true"' : '') + ' tabindex="0">'
  + '<span class="menu-list__label">' + esc(label) + '</span></li>').join('');

$('#thead').innerHTML = T.columns.map((c, i) => {
  const widths = ['22%', '24%', '18%', '17%', '9%', ''];
  const right = i === 5 ? ' tbl-cell--right' : '';
  const label = i === 5
    ? '<span class="sr-only">' + esc(T.actionsHeader) + '</span>'
    : esc(c);
  return '<th scope="col" class="tbl-cell' + right + '"'
       + (widths[i] ? ' style="width:' + widths[i] + '"' : '') + '>'
       + '<span class="tbl-cell__content">' + label + '</span></th>';
}).join('');

const filterSel = $('#stateFilter');
filterSel.innerHTML = '<option value="all">' + esc(T.filterAll) + '</option>'
  + Object.keys(STATE_META).map((k) =>
      '<option value="' + k + '">' + esc(STATE_META[k].label) + '</option>').join('');
filterSel.addEventListener('change', (e) => { filterState = e.target.value; render(); });
$('#q').addEventListener('input', (e) => { query = e.target.value; render(); });

function stampFreshness() {
  const d = new Date();
  const p = (n) => String(n).padStart(2, '0');
  /* variables.css: data and numbers use --font-family-base with tabular-nums —
     the 'Digital Numbers' clock face is reserved for the top-bar timezone
     readout only, so this is plain type with tabular figures. */
  $('#freshness').textContent = T.freshness(p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()));
}
$('#refresh').addEventListener('click', () => { stampFreshness(); render(); });
$('#refreshTop').addEventListener('click', () => { stampFreshness(); render(); });

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

**怎么验**：直接浏览器打开。可交互点 —— 点任一 `Stop Stream` 走二次确认（Esc / Cancel 关闭、Tab 被困在 dialog 内、焦点归还触发按钮、点遮罩**不**关闭）；确认后该行实时降为 Offline 的 `Inactive-Idle` 子型 + 右下角 slide toast；offline 行的 `Stop Stream` 是 disabled 态（ghost/red opacity .4）配 `Start Stream`；第 4 行的长 UDP 地址走 center-ellipsis，hover / 键盘 focus 才弹 tooltip（短值不弹）；最后一行的地址与接收端是 `—` 且**没有** copy 图标。

**没做的两件事，说清楚**：① 我没跑 `pnpm audit:no-hardcoded-design-tokens` / `test:render-verification` 之类的闸 —— 本次会话 Bash 停用，所以「token 零硬编码」是我逐行对着 `variables.css` 写的，不是机检过的；要拿机检证据得在能跑命令的会话里把文件落到 `playground/public/**`（该目录已在 2026-08-11 扩进那条闸的扫描面）再跑。② 上表三条偏差里的第 1 条（Warning pill 变体）需要你拍板才算闭环，我只做到「按同构造式实现 + 显式登记」，没替你决定。