Files
workbuddy_skills/open-design/design-templates/live-artifact/examples/competitor-radar-live.html
T
admin 5afeb6b78a ④段整套同步(六节 + P/S/F/J + 模板)+ open-design 移到技能根
一、④段整套同步(用户选候选 A)
- 新增 `references/stage-proto-doc/assets/产品文档模板.html`(16 KB/247 行,从备份拷、转 LF)。
- ④段 SKILL.md 换成备份 15:28 那版,并按模板口径改齐:
  · 「产品文档框架」从「四块+一附录」改成**六节** —— 页面结构/功能清单/使用场景/操作流程/
    跨页与界面状态/待确认与偏离;与模板同构,⛔ 不从空白页重写结构。
  · 编号从 R/S/F 改成 **P 页面/S 场景/F 功能/J 二级功能**,各自唯一、按展示顺序通排。
  · 保留旧版有价值的要求:每项功能补齐 做什么/入口在哪/输入边界/异常与恢复;重点功能三处标红;
    横向流程图每步写清页面/功能/操作/完成判据。
  · 完成标准「七节」→「六节」,自查清单同步换掉「需求」那批条目,补模板与配图两条。
  · 路径 `designs/<项目>/` → `docs/pm/<项目>/ui/`(2 处)。
  · 补回本机那条「每份产出文档开头都要有一张图」的引用(指向主 skill,段内不复述)。
- 主 skill 同步:四段结构表④行、④段沿革、画图小节(④段画页面结构图 + 用户路径)。

二、open-design 移到技能根(用户选候选 A)
- `git mv product-planning/references/stage-delivery/assets/open-design → open-design/`(4178 文件)。
- `.gitignore` 白名单加 `!/open-design/` —— 否则移出去就不再纳入版本控制。
- 引用改 11 处:路径形态 `assets/open-design/...` → `<技能根>/open-design/...`;
  「随段携带的自带件」改称「同根技能」;「路径怎么找」那条改成自带件(段内相对路径)与
  同根技能(`<技能根>/<技能名>/`)两类分开写。
- open-design 自己的 ATTRIBUTION.md 两处同步(它现在与 oil-ui-pro 同层)。
- 副作用(有益):命名闸门读数由 2063 降到 225 —— 此前它把 open-design 里 919 个第三方 md
  当成规则一起扫了;现在只扫本包 23 个 md。已确认④段的 H1 与 4a/4b/4c 子步仍在检查范围内。

三、验收
- check_naming.py --ws E:/ProgramData/AIProject/contentm_agent = 通过 225 / 失败 0 / 提示 27。
- 活文件里 `assets/open-design` 残留 0;工作区 CRLF 文件 0。
2026-10-15 21:17:28 +08:00

1007 lines
48 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Quiver · Competitor Radar</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
/* Mission-control: cool neutral charcoal */
--bg: #0c0c10;
--bg-soft: #0e1015;
--surface: #14161e;
--surface-2: #181b25;
--surface-3: #1e222e;
--surface-bright: #232734;
--line: #232634;
--line-strong: #303446;
--hairline: rgba(255,255,255,0.04);
--ink: #e7eaf0;
--ink-soft: #b6bbc7;
--muted: #767d8c;
--muted-2: #525866;
--dim: #353a47;
/* Status RGB */
--green: #10b981;
--green-soft: rgba(16,185,129,0.14);
--green-line: rgba(16,185,129,0.5);
--amber: #f59e0b;
--amber-soft: rgba(245,158,11,0.14);
--amber-line: rgba(245,158,11,0.5);
--red: #ef4444;
--red-soft: rgba(239,68,68,0.14);
--red-line: rgba(239,68,68,0.5);
/* Brand */
--cyan: #06b6d4;
--cyan-soft: rgba(6,182,212,0.14);
--violet: #a78bfa;
--violet-soft: rgba(167,139,250,0.16);
/* Type */
--display: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif;
--body: 'Geist', -apple-system, BlinkMacSystemFont, sans-serif;
--mono: 'JetBrains Mono', 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
--r: 8px;
--r-card: 10px;
--r-pill: 999px;
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background:
radial-gradient(900px 500px at 50% 0%, rgba(6,182,212,0.08), transparent 60%),
radial-gradient(700px 400px at 100% 60%, rgba(167,139,250,0.04), transparent 60%),
var(--bg);
color: var(--ink);
font-family: var(--body);
font-size: 13.5px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
/* ─────── Top status bar ─────── */
.statusbar {
border-bottom: 1px solid var(--hairline);
background:
linear-gradient(180deg, rgba(6,182,212,0.05), transparent),
var(--bg-soft);
padding: 8px 24px;
display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
font-family: var(--mono); font-size: 10.5px;
color: var(--muted);
text-transform: uppercase; letter-spacing: 0.1em;
}
.statusbar .system::before {
content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%;
background: var(--green); box-shadow: 0 0 8px var(--green);
margin-right: 8px; vertical-align: 1px;
animation: tick 1.6s ease-in-out infinite;
}
@keyframes tick {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.statusbar .v { color: var(--ink-soft); font-weight: 500; }
.statusbar .item { display: inline-flex; gap: 6px; align-items: center; }
.statusbar .threat-low { color: var(--green); }
.statusbar .threat-mid { color: var(--amber); }
.statusbar .threat-hi { color: var(--red); }
/* ─────── Header ─────── */
header.app {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 24px;
border-bottom: 1px solid var(--hairline);
background: var(--bg);
}
.brand { display: flex; align-items: center; gap: 12px; }
.logo {
width: 30px; height: 30px;
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
background: linear-gradient(135deg, #06b6d4, #0891b2);
box-shadow: 0 0 24px rgba(6,182,212,0.4);
display: inline-flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 13px;
}
.word { font-weight: 700; font-size: 16px; letter-spacing: -0.01em; }
.title-stack { font-family: var(--mono); font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.16em; }
.title-stack .row { color: var(--ink); font-weight: 500; }
.h-right { display: flex; align-items: center; gap: 8px; }
.icon-btn {
height: 32px; padding: 0 12px; border-radius: 6px;
background: var(--surface); border: 1px solid var(--line);
color: var(--ink-soft); font-size: 12px; font-weight: 500;
display: inline-flex; align-items: center; gap: 6px;
transition: all 0.15s ease;
}
.icon-btn:hover { background: var(--surface-2); border-color: var(--line-strong); color: var(--ink); }
.icon-btn.primary {
background: var(--cyan); border-color: var(--cyan); color: #002230;
font-weight: 600;
box-shadow: 0 0 18px rgba(6,182,212,0.3);
}
.icon-btn .ic { width: 13px; height: 13px; }
.icon-btn.spin .ic { animation: spin 0.8s linear; }
@keyframes spin { to { transform: rotate(360deg); } }
/* ─────── Layout ─────── */
main {
max-width: 1480px; margin: 0 auto;
padding: 22px 24px 60px;
display: grid; grid-template-columns: 1fr; gap: 20px;
}
/* ─────── Hero radar + summary ─────── */
.radar-section {
display: grid;
grid-template-columns: 460px 1fr;
gap: 20px;
}
.radar-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-card);
padding: 22px;
position: relative;
overflow: hidden;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.02);
}
.radar-card .head {
display: flex; justify-content: space-between; align-items: flex-start;
margin-bottom: 16px;
}
.radar-card .label {
font-family: var(--mono); font-size: 10px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.16em;
}
.radar-card .title {
font-family: var(--display); font-weight: 600; font-size: 17px;
margin-top: 4px; letter-spacing: -0.005em;
}
.radar-card .frame { color: var(--cyan); font-family: var(--mono); font-size: 10.5px; }
.radar-stage {
position: relative;
width: 100%; aspect-ratio: 1 / 1;
margin: 0 auto;
}
.radar-stage svg { width: 100%; height: 100%; display: block; }
.ring { fill: none; stroke: rgba(6,182,212,0.18); stroke-width: 1; }
.ring.inner { stroke: rgba(6,182,212,0.36); }
.crosshair { stroke: rgba(6,182,212,0.10); stroke-width: 1; }
.ring-label { fill: var(--muted-2); font-family: var(--mono); font-size: 8.5px; text-transform: uppercase; letter-spacing: 0.14em; }
.sweep {
transform-origin: 50% 50%;
animation: sweepRot 6s linear infinite;
}
@keyframes sweepRot { to { transform: rotate(360deg); } }
.radar-node {
position: absolute;
transform: translate(-50%, -50%);
cursor: pointer;
z-index: 3;
}
.radar-node .badge {
width: 38px; height: 38px;
border-radius: 9px;
display: inline-flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 14px;
border: 1.5px solid var(--bg);
box-shadow: 0 4px 14px rgba(0,0,0,0.4);
transition: transform 0.15s, box-shadow 0.15s;
}
.radar-node:hover .badge { transform: scale(1.08); }
.radar-node.active .badge {
box-shadow: 0 0 0 3px var(--cyan), 0 0 24px var(--cyan);
}
.radar-node .pin {
position: absolute; left: 50%; top: 100%;
transform: translate(-50%, 8px);
font-family: var(--mono); font-size: 10px; white-space: nowrap;
color: var(--ink-soft);
background: var(--surface-2);
padding: 2px 6px; border-radius: 4px;
border: 1px solid var(--line);
}
.radar-node .threat-dot {
position: absolute; right: -3px; top: -3px;
width: 10px; height: 10px; border-radius: 50%;
border: 2px solid var(--bg);
z-index: 1;
}
.radar-node[data-threat="hi"] .threat-dot { background: var(--red); box-shadow: 0 0 6px var(--red); animation: tick 1.4s ease-in-out infinite; }
.radar-node[data-threat="mid"] .threat-dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
.radar-node[data-threat="low"] .threat-dot { background: var(--green); box-shadow: 0 0 6px var(--green); }
.legend {
margin-top: 18px;
display: flex; gap: 14px; flex-wrap: wrap;
font-family: var(--mono); font-size: 10.5px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.1em;
}
.legend .swatch { display: inline-flex; gap: 6px; align-items: center; }
.legend .swatch::before { content: ""; width: 8px; height: 8px; border-radius: 50%; }
.legend .hi::before { background: var(--red); }
.legend .mid::before { background: var(--amber); }
.legend .low::before { background: var(--green); }
/* Right side: focused brief */
.focus-card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-card);
overflow: hidden;
display: flex; flex-direction: column;
}
.focus-head {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 18px 22px;
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border-bottom: 1px solid var(--hairline);
}
.focus-head .crumb {
font-family: var(--mono); font-size: 10px; color: var(--muted);
text-transform: uppercase; letter-spacing: 0.16em;
}
.focus-head h2 {
margin: 4px 0 0; font-family: var(--display); font-weight: 600; font-size: 22px;
letter-spacing: -0.015em;
display: inline-flex; gap: 12px; align-items: center;
}
.focus-head .co-mark {
width: 36px; height: 36px; border-radius: 8px;
display: inline-flex; align-items: center; justify-content: center;
color: #fff; font-weight: 700; font-size: 14px;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.2);
}
.focus-head .threat {
font-family: var(--mono); font-size: 10.5px; padding: 4px 10px; border-radius: 999px;
text-transform: uppercase; letter-spacing: 0.1em;
}
.focus-head .threat.hi { background: var(--red-soft); color: var(--red); border: 1px solid var(--red-line); }
.focus-head .threat.mid { background: var(--amber-soft); color: var(--amber); border: 1px solid var(--amber-line); }
.focus-head .threat.low { background: var(--green-soft); color: var(--green); border: 1px solid var(--green-line); }
.focus-stats {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: 1px; background: var(--hairline);
border-bottom: 1px solid var(--hairline);
}
.focus-stat { background: var(--surface); padding: 14px 18px; }
.focus-stat .lbl { font-family: var(--mono); font-size: 9.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.12em; }
.focus-stat .val { font-family: var(--display); font-weight: 600; font-size: 19px; margin-top: 4px; letter-spacing: -0.01em; }
.focus-stat .delta { font-family: var(--mono); font-size: 10.5px; margin-top: 2px; }
.focus-stat .delta.up { color: var(--red); }
.focus-stat .delta.down { color: var(--green); }
.focus-stat .delta.flat { color: var(--muted); }
.focus-thesis {
padding: 18px 22px;
flex: 1;
}
.focus-thesis h4 { font-family: var(--mono); font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.14em; margin: 0 0 10px; font-weight: 500; }
.focus-thesis .body {
color: var(--ink-soft); font-size: 13px; line-height: 1.55;
}
.focus-thesis .body strong { color: var(--ink); }
.focus-thesis .body .accent {
background: linear-gradient(90deg, var(--red), var(--amber));
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.focus-actions {
display: flex; gap: 8px;
padding: 14px 22px;
border-top: 1px solid var(--hairline);
}
.focus-actions .btn {
flex: 1; padding: 9px 12px; border-radius: 6px;
border: 1px solid var(--line); background: var(--surface-2); color: var(--ink);
font-family: var(--display); font-weight: 500; font-size: 12.5px;
transition: all 0.15s;
}
.focus-actions .btn:hover { background: var(--surface-3); border-color: var(--line-strong); }
.focus-actions .btn.primary {
background: var(--cyan); color: #002230; border-color: var(--cyan); font-weight: 600;
}
.focus-actions .btn.primary:hover { box-shadow: 0 0 14px rgba(6,182,212,0.3); }
/* ─────── Filter ribbon ─────── */
.filters {
display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 12px 18px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r);
}
.filter-pills { display: flex; gap: 4px; padding: 3px; background: var(--surface-2); border-radius: 7px; border: 1px solid var(--line); }
.filter-pills button {
border: 0; background: transparent; color: var(--muted);
padding: 5px 11px; font-size: 11.5px; font-weight: 500; border-radius: 5px;
font-family: var(--display);
transition: all 0.15s;
display: inline-flex; gap: 6px; align-items: center;
}
.filter-pills button:hover { color: var(--ink); }
.filter-pills button.active { background: var(--surface-3); color: var(--ink); }
.filter-pills .ic { width: 12px; height: 12px; }
.filter-meta { font-family: var(--mono); font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.1em; }
/* ─────── Activity feed + competitor cards ─────── */
.grid-2 {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 20px;
}
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-card);
overflow: hidden;
}
.panel-head {
display: flex; align-items: center; justify-content: space-between;
padding: 14px 20px; border-bottom: 1px solid var(--hairline);
}
.panel-head .title { font-family: var(--display); font-weight: 600; font-size: 14px; }
.panel-head .sub { font-family: var(--mono); font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.12em; }
.feed { display: flex; flex-direction: column; }
.feed-row {
display: grid;
grid-template-columns: 60px 28px 1fr auto;
gap: 14px;
align-items: center;
padding: 14px 20px;
border-bottom: 1px solid var(--hairline);
cursor: pointer;
transition: background 0.12s;
}
.feed-row:last-child { border-bottom: 0; }
.feed-row:hover { background: var(--surface-2); }
.feed-time { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.feed-mark {
width: 26px; height: 26px; border-radius: 6px;
color: #fff; font-weight: 700; font-size: 11px;
display: inline-flex; align-items: center; justify-content: center;
}
.feed-body .who { font-family: var(--mono); font-size: 10.5px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.08em; }
.feed-body .head { color: var(--ink); font-size: 13px; margin-top: 2px; line-height: 1.4; }
.feed-body .head strong { color: var(--ink); font-weight: 600; }
.feed-body .meta {
font-family: var(--mono); font-size: 10.5px; color: var(--muted);
margin-top: 4px; display: flex; gap: 12px;
}
.feed-tag {
font-family: var(--mono); font-size: 10px; padding: 3px 8px; border-radius: 4px;
text-transform: uppercase; letter-spacing: 0.08em;
}
.feed-tag.hi { background: var(--red-soft); color: var(--red); border: 1px solid var(--red-line); }
.feed-tag.mid { background: var(--amber-soft); color: var(--amber); border: 1px solid var(--amber-line); }
.feed-tag.low { background: var(--green-soft); color: var(--green); border: 1px solid var(--green-line); }
.feed-tag.fyi { background: var(--cyan-soft); color: var(--cyan); border: 1px solid rgba(6,182,212,0.4); }
/* Synthesis card */
.synth {
padding: 18px 20px;
border-bottom: 1px solid var(--hairline);
}
.synth .badge {
display: inline-flex; gap: 6px; align-items: center;
font-family: var(--mono); font-size: 10.5px; color: var(--violet);
text-transform: uppercase; letter-spacing: 0.14em;
}
.synth .badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--violet); box-shadow: 0 0 10px var(--violet); }
.synth h4 {
font-family: var(--display); font-weight: 600; font-size: 18px;
margin: 8px 0 6px; line-height: 1.25; letter-spacing: -0.015em;
}
.synth h4 .accent {
background: linear-gradient(90deg, var(--cyan), var(--violet));
-webkit-background-clip: text; background-clip: text; color: transparent;
}
.synth p {
margin: 0; color: var(--ink-soft); font-size: 12.5px; line-height: 1.55;
}
.synth-list {
margin-top: 14px;
display: flex; flex-direction: column; gap: 10px;
}
.synth-list .item {
display: grid; grid-template-columns: 22px 1fr; gap: 10px; align-items: flex-start;
}
.synth-list .num {
font-family: var(--mono); font-size: 9.5px;
background: var(--violet-soft); color: var(--violet);
border: 1px solid rgba(167,139,250,0.4); border-radius: 4px;
padding: 1px 0; text-align: center; font-weight: 600;
}
.synth-list .text { font-size: 12px; color: var(--ink-soft); line-height: 1.5; }
.synth-list .text strong { color: var(--ink); }
/* Competitor cards */
.comp-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: 1px; background: var(--hairline);
}
.comp-card {
background: var(--surface);
padding: 14px 18px;
cursor: pointer;
transition: background 0.12s;
}
.comp-card:hover { background: var(--surface-2); }
.comp-card.active { background: var(--surface-2); box-shadow: inset 3px 0 0 var(--cyan); }
.comp-top { display: flex; gap: 10px; align-items: center; }
.comp-top .mark {
width: 28px; height: 28px; border-radius: 7px;
color: #fff; font-weight: 700; font-size: 12px;
display: inline-flex; align-items: center; justify-content: center;
}
.comp-top .name { font-weight: 600; font-size: 13px; letter-spacing: -0.005em; }
.comp-top .tier {
margin-left: auto; font-family: var(--mono); font-size: 9.5px;
padding: 2px 6px; border-radius: 3px;
text-transform: uppercase; letter-spacing: 0.1em;
}
.comp-top .tier.hi { background: var(--red-soft); color: var(--red); }
.comp-top .tier.mid { background: var(--amber-soft); color: var(--amber); }
.comp-top .tier.low { background: var(--green-soft); color: var(--green); }
.comp-meta {
margin-top: 10px;
display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
font-size: 11.5px;
}
.comp-meta .row { display: flex; justify-content: space-between; gap: 8px; }
.comp-meta .lbl { color: var(--muted); }
.comp-meta .val { font-family: var(--mono); color: var(--ink); }
.comp-meta .val.up { color: var(--red); }
.comp-meta .val.down { color: var(--green); }
.comp-bar {
margin-top: 10px;
height: 28px;
background: var(--surface-3);
border-radius: 5px;
position: relative; overflow: hidden;
}
.comp-bar svg { width: 100%; height: 100%; display: block; }
/* ─────── Toast ─────── */
.toast-box {
position: fixed; bottom: 24px; right: 24px; z-index: 200;
display: flex; flex-direction: column; gap: 10px;
pointer-events: none;
}
.toast {
pointer-events: auto;
min-width: 280px; max-width: 380px;
padding: 12px 14px;
background: rgba(20,22,30,0.95);
border: 1px solid var(--line);
border-left: 3px solid var(--cyan);
border-radius: 8px;
backdrop-filter: blur(16px);
box-shadow: 0 16px 48px rgba(0,0,0,0.5);
font-size: 12.5px; color: var(--ink);
animation: toastIn 0.32s cubic-bezier(0.2, 0.8, 0.2, 1);
}
.toast.hi { border-left-color: var(--red); }
.toast.mid { border-left-color: var(--amber); }
.toast.low { border-left-color: var(--green); }
.toast.violet { border-left-color: var(--violet); }
.toast .t-title { font-weight: 600; font-size: 12.5px; margin-bottom: 2px; font-family: var(--mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-soft); }
.toast .t-body { color: var(--ink); line-height: 1.45; }
.toast .t-body strong { color: var(--ink); }
.toast.fade { opacity: 0; transform: translateX(20px); transition: all 0.25s; }
@keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
@media (max-width: 1080px) {
.radar-section { grid-template-columns: 1fr; }
.grid-2 { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.focus-stats { grid-template-columns: 1fr 1fr; }
.comp-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<!-- ─────── Status bar ─────── -->
<div class="statusbar">
<span class="item system"><span>System</span><span class="v">nominal · 5 sources active</span></span>
<span class="item"><span>Threat level</span><span class="v threat-mid">elevated</span></span>
<span class="item"><span>Signals (24h)</span><span class="v" id="sigCount">42</span></span>
<span class="item"><span>Last scan</span><span class="v" id="lastScan">3m ago</span></span>
<span class="item" style="margin-left:auto"><span>Watchlist</span><span class="v">5 competitors · 18 keywords</span></span>
</div>
<!-- ─────── Header ─────── -->
<header class="app">
<div class="brand">
<div class="logo">Q</div>
<div class="word">quiver</div>
<div class="title-stack">
<div>Competitor Intelligence</div>
<div class="row">Q2 Watch · pmtools.dev cohort</div>
</div>
</div>
<div class="h-right">
<button class="icon-btn" id="digestBtn">
<svg class="ic" viewBox="0 0 16 16" fill="none"><path d="M3 5h10M3 8h10M3 11h7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
Generate weekly digest
</button>
<button class="icon-btn primary" id="scanBtn">
<svg class="ic" viewBox="0 0 16 16" fill="none"><path d="M14 8a6 6 0 1 1-1.76-4.24M14 3v3.5h-3.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Scan now
</button>
</div>
</header>
<main>
<!-- ─────── Radar + focused brief ─────── -->
<section class="radar-section">
<div class="radar-card">
<div class="head">
<div>
<div class="label">Active surveillance</div>
<div class="title">Distance = threat level · Color = trend</div>
</div>
<div class="frame">FRAME 04 / 09</div>
</div>
<div class="radar-stage" id="radarStage">
<svg viewBox="0 0 400 400" preserveAspectRatio="xMidYMid meet">
<defs>
<radialGradient id="bgRing" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#1f2738" stop-opacity="0.6"/>
<stop offset="60%" stop-color="#1f2738" stop-opacity="0.18"/>
<stop offset="100%" stop-color="#1f2738" stop-opacity="0"/>
</radialGradient>
<linearGradient id="sweepGrad" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stop-color="rgba(6,182,212,0)"/>
<stop offset="80%" stop-color="rgba(6,182,212,0.18)"/>
<stop offset="100%" stop-color="rgba(6,182,212,0.55)"/>
</linearGradient>
</defs>
<circle cx="200" cy="200" r="195" fill="url(#bgRing)"/>
<!-- Concentric rings -->
<circle class="ring inner" cx="200" cy="200" r="50"/>
<circle class="ring" cx="200" cy="200" r="100"/>
<circle class="ring" cx="200" cy="200" r="150"/>
<circle class="ring" cx="200" cy="200" r="190"/>
<line class="crosshair" x1="200" y1="10" x2="200" y2="390"/>
<line class="crosshair" x1="10" y1="200" x2="390" y2="200"/>
<line class="crosshair" x1="60" y1="60" x2="340" y2="340"/>
<line class="crosshair" x1="340" y1="60" x2="60" y2="340"/>
<!-- Ring labels -->
<text class="ring-label" x="200" y="146" text-anchor="middle">Critical</text>
<text class="ring-label" x="200" y="96" text-anchor="middle">High</text>
<text class="ring-label" x="200" y="46" text-anchor="middle">Watch</text>
<!-- Sweep -->
<g class="sweep">
<path d="M200,200 L200,10 A190,190 0 0,1 380,200 Z" fill="url(#sweepGrad)" opacity="0.9"/>
</g>
<!-- Center marker -->
<circle cx="200" cy="200" r="3" fill="#06b6d4"/>
<circle cx="200" cy="200" r="6" fill="none" stroke="#06b6d4" stroke-opacity="0.6"/>
</svg>
<!-- Competitor nodes (positioned in % so they scale with stage) -->
<div class="radar-node" data-id="linear" data-threat="hi" style="left:62%;top:30%">
<span class="badge" style="background:linear-gradient(135deg,#5e6ad2,#3a44a0)">L</span>
<span class="threat-dot"></span>
<span class="pin">Linear</span>
</div>
<div class="radar-node" data-id="notion" data-threat="mid" style="left:30%;top:42%">
<span class="badge" style="background:linear-gradient(135deg,#1f1f1f,#3a3a3a)">N</span>
<span class="threat-dot"></span>
<span class="pin">Notion</span>
</div>
<div class="radar-node" data-id="asana" data-threat="low" style="left:24%;top:74%">
<span class="badge" style="background:linear-gradient(135deg,#f06a6a,#ad3838)">A</span>
<span class="threat-dot"></span>
<span class="pin">Asana</span>
</div>
<div class="radar-node" data-id="monday" data-threat="hi" style="left:74%;top:62%">
<span class="badge" style="background:linear-gradient(135deg,#ff3d57,#9c2236)">M</span>
<span class="threat-dot"></span>
<span class="pin">Monday</span>
</div>
<div class="radar-node" data-id="clickup" data-threat="mid" style="left:54%;top:78%">
<span class="badge" style="background:linear-gradient(135deg,#7b68ee,#4d3fb6)">C</span>
<span class="threat-dot"></span>
<span class="pin">ClickUp</span>
</div>
</div>
<div class="legend">
<span class="swatch hi">Critical · move now</span>
<span class="swatch mid">Watch · weekly</span>
<span class="swatch low">Stable</span>
</div>
</div>
<!-- Focused brief on selected competitor -->
<div class="focus-card" id="focusCard">
<div class="focus-head">
<div>
<div class="crumb">Selected · 04 May ↗ 06 May</div>
<h2 id="focusTitle"><span class="co-mark" id="focusMark" style="background:linear-gradient(135deg,#5e6ad2,#3a44a0)">L</span> Linear</h2>
</div>
<span class="threat hi" id="focusThreat">Critical · 92</span>
</div>
<div class="focus-stats">
<div class="focus-stat">
<div class="lbl">Pricing</div>
<div class="val" id="fsPrice">$8 → $10</div>
<div class="delta up" id="fsPriceDelta">+25% Pro · 2d ago</div>
</div>
<div class="focus-stat">
<div class="lbl">Releases (30d)</div>
<div class="val" id="fsRel">7</div>
<div class="delta up" id="fsRelDelta">+3 vs cohort avg</div>
</div>
<div class="focus-stat">
<div class="lbl">Hiring</div>
<div class="val" id="fsHire">14 open roles</div>
<div class="delta up" id="fsHireDelta">+8 in 30d · GTM heavy</div>
</div>
<div class="focus-stat">
<div class="lbl">X / Twitter velocity</div>
<div class="val" id="fsTw">142k impr/d</div>
<div class="delta up" id="fsTwDelta">+62% vs 30d avg</div>
</div>
</div>
<div class="focus-thesis">
<h4>Quiver synthesis · why this matters</h4>
<div class="body" id="focusBody">
<strong class="accent">Linear is hardening pricing into the AI-features bundle.</strong> The Pro plan jumped +25% with two announced AI features (auto-triage, code-search). Their hiring pipeline is GTM-heavy (8 of 14 new) — suggests outbound push, not inbound. <strong>Counter-move:</strong> emphasize cost-of-AI-features in our positioning and pre-empt with a 2-week migration tool.
</div>
</div>
<div class="focus-actions">
<button class="btn primary" data-fa="alert">⚑ Set alert on this competitor</button>
<button class="btn" data-fa="brief">Send brief to team</button>
<button class="btn" data-fa="open">Open intel page</button>
</div>
</div>
</section>
<!-- ─────── Filters ─────── -->
<div class="filters">
<div class="filter-pills" id="filterPills">
<button class="active" data-f="all">All <span class="ic" style="opacity:0.5;font-size:9px">42</span></button>
<button data-f="pricing">
<svg class="ic" viewBox="0 0 16 16" fill="none"><path d="M3 8h10M5 5l-2 3 2 3M11 5l2 3-2 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Pricing
</button>
<button data-f="product">
<svg class="ic" viewBox="0 0 16 16" fill="none"><rect x="3" y="3" width="10" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 6h10M6 3v10" stroke="currentColor" stroke-width="1.4"/></svg>
Product
</button>
<button data-f="hiring">
<svg class="ic" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="6" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M3 13c0-2.5 2-4 5-4s5 1.5 5 4" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>
Hiring
</button>
<button data-f="social">
<svg class="ic" viewBox="0 0 16 16" fill="none"><path d="M3 13l3-3 3 3 4-7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
Social
</button>
<button data-f="press">
<svg class="ic" viewBox="0 0 16 16" fill="none"><rect x="3" y="3" width="10" height="10" rx="1" stroke="currentColor" stroke-width="1.4"/><path d="M5 6h6M5 8h6M5 10h4" stroke="currentColor" stroke-width="1.4"/></svg>
Press
</button>
</div>
<div class="filter-meta"><span id="feedCount">42</span> events · sorted by impact</div>
</div>
<!-- ─────── AI synthesis + activity feed + competitor cards ─────── -->
<section class="grid-2">
<!-- Activity feed -->
<div class="card">
<div class="panel-head">
<span class="title">Activity feed</span>
<span class="sub">Live · 24h window</span>
</div>
<div class="feed" id="feed"></div>
</div>
<!-- Right column: AI synthesis + competitor mini-cards -->
<div style="display:flex;flex-direction:column;gap:20px;">
<div class="card">
<div class="panel-head">
<span class="title">Quiver AI synthesis</span>
<span class="sub">Updated 14 min ago</span>
</div>
<div class="synth">
<div class="badge">Quiver intel</div>
<h4>The cohort just made <span class="accent">two converging moves</span> on AI features.</h4>
<p>Linear and Monday both raised Pro pricing with AI-bundle messaging. ClickUp's enterprise outbound stepped up <strong>+62%</strong> in social. Notion stayed quiet — possibly preparing a larger announcement.</p>
<div class="synth-list">
<div class="item"><div class="num">01</div><div class="text"><strong>Pre-empt the AI-bundle frame</strong> — publish "AI features included, no markup" landing page this week.</div></div>
<div class="item"><div class="num">02</div><div class="text"><strong>Migration tool from Linear → us</strong> — historical pattern: 2-week post-price-hike migration windows convert 3-5×.</div></div>
<div class="item"><div class="num">03</div><div class="text"><strong>Watch Notion next 7 days</strong> — silence + GTM hires usually precedes a major launch (last seen Sep '25 calendar release).</div></div>
</div>
</div>
</div>
<div class="card">
<div class="panel-head">
<span class="title">Competitor scorecard</span>
<span class="sub">Click to focus</span>
</div>
<div class="comp-grid" id="compGrid"></div>
</div>
</div>
</section>
</main>
<div class="toast-box" id="toastBox"></div>
<script>
(() => {
// ─────── Competitors ───────
const COMPS = {
linear: {
name: 'Linear', mark: 'L', bg: 'linear-gradient(135deg,#5e6ad2,#3a44a0)',
threat: 'hi', score: 92,
stats: { price: '$8 → $10', priceDelta: '+25% Pro · 2d ago', priceDir: 'up',
releases: '7', releasesDelta: '+3 vs cohort avg', releasesDir: 'up',
hiring: '14 open roles', hiringDelta: '+8 in 30d · GTM heavy', hiringDir: 'up',
twitter: '142k impr/d', twitterDelta: '+62% vs 30d avg', twitterDir: 'up' },
synthesis: '<strong class="accent">Linear is hardening pricing into the AI-features bundle.</strong> The Pro plan jumped +25% with two announced AI features (auto-triage, code-search). Their hiring pipeline is GTM-heavy (8 of 14 new) — suggests outbound push, not inbound. <strong>Counter-move:</strong> emphasize cost-of-AI-features in our positioning and pre-empt with a 2-week migration tool.',
micro: 'Pricing +25% · 7 releases · 142k impr/d',
bar: 88
},
notion: {
name: 'Notion', mark: 'N', bg: 'linear-gradient(135deg,#1f1f1f,#3a3a3a)',
threat: 'mid', score: 71,
stats: { price: '$10 (flat)', priceDelta: 'no change · 6mo+', priceDir: 'flat',
releases: '4', releasesDelta: 'avg pace · maintenance', releasesDir: 'flat',
hiring: '22 open roles', hiringDelta: '+11 in 30d · platform & infra', hiringDir: 'up',
twitter: '88k impr/d', twitterDelta: '−4% vs 30d avg', twitterDir: 'down' },
synthesis: '<strong class="accent">Suspicious silence.</strong> Notion is unusually quiet on social, but their hiring is heavily weighted to platform + infra (11 of 22 new). The pattern matches Sep \'25, two months before Calendar shipped. <strong>Watch:</strong> expect a larger announcement in 30–45 days.',
micro: 'Quiet on social · 11 platform hires · pattern match',
bar: 62
},
asana: {
name: 'Asana', mark: 'A', bg: 'linear-gradient(135deg,#f06a6a,#ad3838)',
threat: 'low', score: 38,
stats: { price: '$13.49 (flat)', priceDelta: 'last change Jan \'25', priceDir: 'flat',
releases: '2', releasesDelta: '−2 vs cohort · slow', releasesDir: 'down',
hiring: '6 open roles', hiringDelta: 'flat · half on EU expansion', hiringDir: 'flat',
twitter: '34k impr/d', twitterDelta: '−18% vs 30d avg', twitterDir: 'down' },
synthesis: '<strong>Asana is in maintenance mode.</strong> Two releases in 30 days, both bug-only. Hiring frozen except EU sales. Twitter velocity sliding. <strong>Read:</strong> not a near-term threat. Watch for a leadership change or pricing reset as the trigger.',
micro: 'Slow releases · −18% impressions · EU-only hiring',
bar: 24
},
monday: {
name: 'Monday', mark: 'M', bg: 'linear-gradient(135deg,#ff3d57,#9c2236)',
threat: 'hi', score: 84,
stats: { price: '$9 → $12', priceDelta: '+33% Standard · 5d ago', priceDir: 'up',
releases: '6', releasesDelta: '+2 vs cohort · AI-flavored', releasesDir: 'up',
hiring: '38 open roles', hiringDelta: '+18 in 30d · enterprise blitz', hiringDir: 'up',
twitter: '210k impr/d', twitterDelta: '+118% vs 30d avg', twitterDir: 'up' },
synthesis: '<strong class="accent">Monday is going aggressive on enterprise.</strong> Pricing +33%, 18 enterprise GTM hires, social impressions doubled. Sales motion shifted from PLG to outbound on mid-market accounts. <strong>Counter-move:</strong> reinforce self-serve story; outbound pricing flexibility for $50–200k ARR band.',
micro: 'Pricing +33% · 18 GTM hires · 210k impr/d',
bar: 92
},
clickup: {
name: 'ClickUp', mark: 'C', bg: 'linear-gradient(135deg,#7b68ee,#4d3fb6)',
threat: 'mid', score: 64,
stats: { price: '$7 (flat)', priceDelta: 'undercutter · holding', priceDir: 'flat',
releases: '12', releasesDelta: '+8 vs cohort · feature-tank', releasesDir: 'up',
hiring: '11 open roles', hiringDelta: '+3 in 30d · steady', hiringDir: 'up',
twitter: '94k impr/d', twitterDelta: '+18% vs 30d avg', twitterDir: 'up' },
synthesis: '<strong>ClickUp is shipping fast and undercutting on price.</strong> 12 releases in 30 days — feature-tank strategy. Pricing held at $7 while peers raised. <strong>Risk:</strong> commoditizing the mid-tier. <strong>Counter:</strong> lean into deeper integrations vs surface area.',
micro: '12 releases · price flat at $7 · feature-tank',
bar: 70
}
};
// ─────── Activity feed ───────
const FEED = [
{ time: '14:32', who: 'monday.com', mark: 'M', bg: '#ff3d57', tag: 'pricing', tagClass: 'hi', head: 'Pricing change: <strong>Standard +33%</strong> ($9 → $12), Pro +18%. Effective immediately for new accounts; 30-day grandfather for existing.', meta: 'Source: monday.com/pricing · Detected by Quiver crawler · Confirmed by 2 user posts' },
{ time: '13:18', who: 'Linear', mark: 'L', bg: '#5e6ad2', tag: 'product', tagClass: 'hi', head: 'Released <strong>auto-triage</strong> + AI-search at the <em>Pro</em> tier — bundled with the +25% price hike from May 4.', meta: 'Source: linear.app/changelog · 2.3k Twitter mentions in 6h' },
{ time: '11:46', who: 'ClickUp', mark: 'C', bg: '#7b68ee', tag: 'product', tagClass: 'mid', head: 'Shipped <strong>4 features in 36 hours</strong>: AI image gen, Slack 2-way sync, table view tweaks, mobile widgets.', meta: 'Source: clickup.com/changelog · feature-tank velocity confirmed' },
{ time: '10:22', who: 'Notion', mark: 'N', bg: '#1f1f1f', tag: 'hiring', tagClass: 'mid', head: 'Posted <strong>11 platform/infra roles</strong> in 30 days — Senior Distributed Systems, Search Infra, Realtime Sync. Salary band $260–340k.', meta: 'Source: notion.so/careers · Levels.fyi cross-check' },
{ time: '09:48', who: 'Linear', mark: 'L', bg: '#5e6ad2', tag: 'social', tagClass: 'hi', head: 'Karri Saarinen viral thread on AI-triage hit <strong>1.4M impressions</strong> · 2.8k retweets. Sentiment 78% positive.', meta: 'Source: x.com/karrisaarinen · Tweet ID 1789... · ROI ~$284k earned media' },
{ time: '09:04', who: 'monday.com', mark: 'M', bg: '#ff3d57', tag: 'hiring', tagClass: 'hi', head: 'Opened <strong>18 enterprise GTM roles</strong> in 30 days — AE, SE, CSM concentrated in $50k+ band.', meta: 'Source: monday.com/jobs · Outbound focus confirmed' },
{ time: '08:30', who: 'Asana', mark: 'A', bg: '#f06a6a', tag: 'press', tagClass: 'fyi', head: 'CEO interview in The Information: "We\'re slowing platform investment to focus on Goals."', meta: 'Source: theinformation.com · Strategic narrowing — bullish read for our team' },
{ time: '07:12', who: 'ClickUp', mark: 'C', bg: '#7b68ee', tag: 'social', tagClass: 'mid', head: 'Founder posted demo video of new AI Brain — <strong>312k views</strong>, 940 retweets, mixed reception in replies.', meta: 'Source: x.com/zebzanaib · Reception: positive 56%, skeptical 31%' },
{ time: 'Yest 22:48', who: 'Linear', mark: 'L', bg: '#5e6ad2', tag: 'pricing', tagClass: 'hi', head: 'Pricing page <strong>copy-tested</strong> 3 variations of "AI included" headline (split A/B/C).', meta: 'Source: Quiver tracker · Hashed copy matches 18% of competitor lines' },
{ time: 'Yest 19:14', who: 'Notion', mark: 'N', bg: '#1f1f1f', tag: 'product', tagClass: 'fyi', head: 'Quiet update: <strong>Calendar API</strong> deprecated v1 endpoints — webhooks shifted format. Indicates platform investment.', meta: 'Source: developers.notion.com · Cross-confirmed via deprecation feed' }
];
const feedEl = document.getElementById('feed');
const compGridEl = document.getElementById('compGrid');
const feedCount = document.getElementById('feedCount');
function renderFeed(filter = 'all') {
const items = filter === 'all' ? FEED : FEED.filter(x => x.tag === filter);
feedCount.textContent = items.length;
feedEl.innerHTML = items.map(f => `
<div class="feed-row" data-who="${f.who.toLowerCase().split(' ')[0].replace('.com','')}">
<div class="feed-time">${f.time}</div>
<span class="feed-mark" style="background:${f.bg}">${f.mark}</span>
<div class="feed-body">
<div class="who">${f.who} · ${f.tag}</div>
<div class="head">${f.head}</div>
<div class="meta"><span>${f.meta}</span></div>
</div>
<span class="feed-tag ${f.tagClass}">${f.tagClass === 'hi' ? 'critical' : f.tagClass === 'mid' ? 'watch' : f.tagClass === 'low' ? 'stable' : 'fyi'}</span>
</div>
`).join('');
feedEl.querySelectorAll('.feed-row').forEach(row => {
row.addEventListener('click', () => {
toast({ kind: 'low', title: 'Event opened', body: row.querySelector('.head').innerHTML });
});
});
}
renderFeed('all');
// ─────── Competitor cards ───────
function renderComps() {
compGridEl.innerHTML = Object.entries(COMPS).map(([id, c]) => `
<div class="comp-card" data-id="${id}">
<div class="comp-top">
<span class="mark" style="background:${c.bg}">${c.mark}</span>
<span class="name">${c.name}</span>
<span class="tier ${c.threat}">${c.threat === 'hi' ? 'Critical' : c.threat === 'mid' ? 'Watch' : 'Stable'} · ${c.score}</span>
</div>
<div class="comp-meta">
<div class="row"><span class="lbl">Pricing</span><span class="val ${c.stats.priceDir}">${c.stats.price}</span></div>
<div class="row"><span class="lbl">Hiring</span><span class="val ${c.stats.hiringDir}">${c.stats.hiring.replace(/ open roles/, '')}</span></div>
<div class="row"><span class="lbl">Releases</span><span class="val ${c.stats.releasesDir}">${c.stats.releases}/30d</span></div>
<div class="row"><span class="lbl">Twitter</span><span class="val ${c.stats.twitterDir}">${c.stats.twitter}</span></div>
</div>
<div class="comp-bar">
<svg viewBox="0 0 200 28" preserveAspectRatio="none">
<defs><linearGradient id="cg-${id}" x1="0" x2="1" y1="0" y2="0">
<stop offset="0%" stop-color="#06b6d4" stop-opacity="0.6"/>
<stop offset="100%" stop-color="${c.threat === 'hi' ? '#ef4444' : c.threat === 'mid' ? '#f59e0b' : '#10b981'}" stop-opacity="0.8"/>
</linearGradient></defs>
<rect x="0" y="0" width="${c.bar * 2}" height="28" fill="url(#cg-${id})"/>
<text x="${Math.min(190, c.bar * 2 + 6)}" y="18" font-family="JetBrains Mono,monospace" font-size="10" fill="#e7eaf0">${c.micro}</text>
</svg>
</div>
</div>
`).join('');
compGridEl.querySelectorAll('.comp-card').forEach(card => {
card.addEventListener('click', () => {
focus(card.dataset.id);
});
});
}
renderComps();
// ─────── Focus / select ───────
let activeId = 'linear';
function focus(id) {
if (!COMPS[id]) return;
activeId = id;
const c = COMPS[id];
// Update focus card
document.getElementById('focusTitle').innerHTML = `<span class="co-mark" id="focusMark" style="background:${c.bg}">${c.mark}</span> ${c.name}`;
const t = document.getElementById('focusThreat');
t.className = `threat ${c.threat}`;
t.textContent = (c.threat === 'hi' ? 'Critical · ' : c.threat === 'mid' ? 'Watch · ' : 'Stable · ') + c.score;
document.getElementById('fsPrice').textContent = c.stats.price;
document.getElementById('fsRel').textContent = c.stats.releases;
document.getElementById('fsHire').textContent = c.stats.hiring;
document.getElementById('fsTw').textContent = c.stats.twitter;
document.getElementById('fsPriceDelta').textContent = c.stats.priceDelta;
document.getElementById('fsRelDelta').textContent = c.stats.releasesDelta;
document.getElementById('fsHireDelta').textContent = c.stats.hiringDelta;
document.getElementById('fsTwDelta').textContent = c.stats.twitterDelta;
['fsPrice','fsRel','fsHire','fsTw'].forEach((idn, i) => {
const dirs = [c.stats.priceDir, c.stats.releasesDir, c.stats.hiringDir, c.stats.twitterDir];
const el = document.getElementById(idn + 'Delta');
el.className = `delta ${dirs[i]}`;
});
document.getElementById('focusBody').innerHTML = c.synthesis;
// Highlight radar node + comp card
document.querySelectorAll('.radar-node').forEach(n => n.classList.toggle('active', n.dataset.id === id));
document.querySelectorAll('.comp-card').forEach(n => n.classList.toggle('active', n.dataset.id === id));
}
document.querySelectorAll('.radar-node').forEach(n => {
n.addEventListener('click', () => {
focus(n.dataset.id);
toast({ kind: COMPS[n.dataset.id].threat, title: COMPS[n.dataset.id].name, body: `Focused · threat <strong>${COMPS[n.dataset.id].threat === 'hi' ? 'critical' : COMPS[n.dataset.id].threat === 'mid' ? 'watch' : 'stable'} · ${COMPS[n.dataset.id].score}</strong>` });
});
});
// ─────── Filter ───────
document.querySelectorAll('#filterPills button').forEach(b => {
b.addEventListener('click', () => {
document.querySelectorAll('#filterPills button').forEach(x => x.classList.toggle('active', x === b));
renderFeed(b.dataset.f);
});
});
// ─────── Toasts ───────
const toastBox = document.getElementById('toastBox');
function toast(opts) {
const { kind = '', title = '', body = '' } = (typeof opts === 'string') ? { body: opts } : opts;
const node = document.createElement('div');
node.className = `toast ${kind}`;
node.innerHTML = `<div class="t-title">${title}</div><div class="t-body">${body}</div>`;
toastBox.appendChild(node);
setTimeout(() => {
node.classList.add('fade');
setTimeout(() => node.remove(), 280);
}, 4200);
}
// ─────── Scan now ───────
const scanBtn = document.getElementById('scanBtn');
let mins = 3;
scanBtn.addEventListener('click', () => {
scanBtn.classList.add('spin');
scanBtn.disabled = true;
setTimeout(() => {
scanBtn.classList.remove('spin');
scanBtn.disabled = false;
mins = 0;
document.getElementById('lastScan').textContent = 'just now';
// Bump signals count + add a synthetic feed entry at top
const sigEl = document.getElementById('sigCount');
sigEl.textContent = (parseInt(sigEl.textContent) + 3).toString();
FEED.unshift({ time: '14:36', who: 'Linear', mark: 'L', bg: '#5e6ad2', tag: 'social', tagClass: 'hi', head: '<strong>New:</strong> Pricing page A/B test concluded — variant C "<em>AI features included</em>" wins by 18%. Going live tomorrow.', meta: 'Source: Quiver tracker · Detected via copy delta · Confidence 94%' });
renderFeed(document.querySelector('#filterPills .active').dataset.f);
toast({ kind: 'hi', title: 'Scan complete', body: `<strong>3 new signals</strong> · 1 critical · 2 watch<br/>Linear A/B test concluded — pricing copy change inbound.` });
}, 1100);
});
setInterval(() => {
mins += 1;
document.getElementById('lastScan').textContent = mins === 0 ? 'just now' : `${mins}m ago`;
}, 60000);
// ─────── Other actions ───────
document.getElementById('digestBtn').addEventListener('click', () => {
toast({ kind: 'violet', title: 'Weekly digest drafted', body: '5 competitors · 42 signals · 3 recommended actions · ready in Notion at <strong>Q2 Intel / Week 18</strong>.' });
});
document.querySelectorAll('[data-fa]').forEach(b => {
b.addEventListener('click', () => {
const a = b.dataset.fa;
const c = COMPS[activeId];
const map = {
alert: { kind: 'mid', title: 'Alert set', body: `Will ping you on any <strong>${c.name}</strong> pricing or product change ≥ medium confidence.` },
brief: { kind: '', title: 'Brief sent', body: `${c.name} brief sent to #competitive Slack channel · 4 takeaways · 1 recommended action.` },
open: { kind: '', title: c.name + ' intel page', body: 'Opening full timeline · 30-day pricing diff · 90-day social heatmap.' }
};
if (map[a]) toast(map[a]);
});
});
focus('linear'); // initial highlight
})();
</script>
</body>
</html>