diff --git a/docs/design/artboards/d215/cap/measure_after.js b/docs/design/artboards/d215/cap/measure_after.js
index 865f4c990..b54c10ee3 100644
--- a/docs/design/artboards/d215/cap/measure_after.js
+++ b/docs/design/artboards/d215/cap/measure_after.js
@@ -18,7 +18,7 @@ const fs = require('fs');
const HERE = __dirname;
const TARGETS = path.join(HERE, '..', 'targets.json');
-const SURFACES = ['settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'byo-handoff', 'firstrun-welcome', 'firstrun-waiting', 'firstrun-connected', 'firstrun-error'];
+const SURFACES = ['settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'byo-handoff', 'firstrun-welcome', 'firstrun-waiting', 'firstrun-connected', 'firstrun-error', 'connect'];
// byo-handoff is drawn but can't be captured on prod: reaching it issues a real key. The first-run
// dialog is a latch-gated overlay, and the Scout welcome only exists in a brand-new pod, so neither is measured on prod.
const PROD_SURFACES = SURFACES.filter(s => s !== 'byo-handoff' && !s.startsWith('firstrun-'));
@@ -165,6 +165,7 @@ async function prod(only) {
['board', () => `/v2/pods/${pod}/board`],
['connectors', () => '/v2/connectors'],
['byo', () => '/v2/agents/byo'],
+ ['connect', () => '/v2/connect'],
['chat', () => `/v2/pods/${pod}`],
['sidebar', vp => (vp === '1440' ? `/v2/pods/${side}` : '/v2'), { list390: SIDEBAR_390 }],
];
diff --git a/docs/design/artboards/d215/cap/measure_after.test.js b/docs/design/artboards/d215/cap/measure_after.test.js
index 33997bb53..d40f3a3e1 100644
--- a/docs/design/artboards/d215/cap/measure_after.test.js
+++ b/docs/design/artboards/d215/cap/measure_after.test.js
@@ -29,7 +29,7 @@ test('--only with an unknown surface throws', () => {
test('the committed targets.json passes its own validation', () => {
const t = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'targets.json'), 'utf8'));
- assert.strictEqual(Object.keys(t).length, 26);
+ assert.strictEqual(Object.keys(t).length, 28);
validateTargets(t);
});
@@ -39,7 +39,7 @@ test('an empty or partial measurement is refused', () => {
const partial = { ...t }; delete partial['board-390'];
assert.throws(() => validateTargets(partial), /missing board-390/);
assert.throws(() => validateTargets({ ...t, 'chat-1440': { helperWords: NaN, innerRatio: 2 } }), /invalid chat-1440/);
- assert.strictEqual(EXPECTED.length, 26);
+ assert.strictEqual(EXPECTED.length, 28);
});
// A page whose SPA shell answers 200 and then redirects to `landsOn`. Its metrics would score
diff --git a/docs/design/artboards/d215/index.html b/docs/design/artboards/d215/index.html
index 50eb1d71b..3f4e0adfb 100644
--- a/docs/design/artboards/d215/index.html
+++ b/docs/design/artboards/d215/index.html
@@ -489,6 +489,34 @@
.app.m .v2-feature--byo .v2-byo__copy { position: static; width: 100%; height: 44px; margin-top: 10px; font-size: 14px; }
.app.m .v2-feature--byo .v2-byo__more > summary { min-height: 48px; padding: 8px 16px 8px 12px; }
+/* connect: the page's own v2-connect__* names, in the connectors family (key-gradient Copy, panel code
+ blocks like BYO's command, mono labels). Scoped to .v2-connect, the page's root. */
+.v2-connect { display: flex; flex-direction: column; gap: 16px; max-width: 960px; margin: 0 auto; }
+.v2-connect .v2-connect__lane { display: flex; flex-direction: column; gap: 14px; padding: 20px; border-radius: 14px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); }
+.v2-connect .v2-connect__lane-heading { display: flex; align-items: baseline; gap: 10px; }
+.v2-connect .v2-connect__number { flex: none; font: 500 11.5px/1 var(--f-mono); color: var(--c-text); }
+.v2-connect .v2-connect__lane h2 { margin: 0; font: 800 16px/1.25 var(--f-display); letter-spacing: -0.02em; color: var(--ink); }
+.v2-connect .v2-connect__code-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
+.v2-connect .v2-connect__code-card { min-width: 0; overflow: hidden; border-radius: 12px; background: var(--p); box-shadow: inset 0 0 0 1px var(--d); }
+.v2-connect .v2-connect__code-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--d); font: 500 11.5px/16px var(--f-mono); color: var(--mu); }
+.v2-connect .v2-connect__code-card pre { margin: 0; padding: 12px 14px; font: 500 12.5px/21px var(--f-mono); color: var(--ink); white-space: pre-wrap; overflow-wrap: anywhere; }
+.v2-connect .v2-connect__copy { all: unset; box-sizing: border-box; cursor: pointer; position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 12px; border-radius: 8px; font: 600 12.5px/1 var(--f-body); color: var(--ink); background: linear-gradient(180deg, var(--key-a), var(--key-b)); border: 1px solid var(--key-edge); box-shadow: var(--sh-key); }
+.v2-connect .v2-connect__rule .v2-connect__code-head span { min-width: 0; }
+.v2-connect .v2-connect__rule .v2-connect__code-head code { font: 500 11.5px/16px var(--f-mono); color: var(--ink); overflow-wrap: anywhere; }
+.v2-connect .v2-connect__cursor { display: flex; flex-wrap: wrap; align-items: center; gap: 0 12px; font-size: 13px; line-height: 20px; color: var(--se); }
+.v2-connect .v2-connect__link { display: inline-flex; align-items: center; min-height: 32px; font: 600 13px/20px var(--f-body); color: var(--c-text); text-decoration: none; }
+.v2-connect .v2-connect__lane > .v2-connect__link, .v2-connect .v2-connect__lane > .v2-connect__secondary { align-self: flex-start; }
+.v2-connect .v2-connect__actions { display: flex; flex-wrap: wrap; gap: 8px; }
+.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--f-body); white-space: nowrap; text-decoration: none; cursor: pointer; }
+.v2-connect .v2-connect__cta { color: var(--on-fill); background: linear-gradient(180deg, var(--fill-a), var(--fill-b)); border: 1px solid var(--fill-edge); }
+.v2-connect .v2-connect__secondary { color: var(--ink); background: linear-gradient(180deg, var(--key-a), var(--key-b)); border: 1px solid var(--key-edge); box-shadow: var(--sh-key); }
+.app.m .v2-connect { gap: 10px; }
+.app.m .v2-connect .v2-connect__lane { padding: 16px; }
+.app.m .v2-connect .v2-connect__code-grid { grid-template-columns: minmax(0, 1fr); }
+.app.m .v2-connect .v2-connect__link { min-height: 44px; }
+.app.m .v2-connect .v2-connect__actions { flex-direction: column; }
+.app.m .v2-connect .v2-connect__cta, .app.m .v2-connect .v2-connect__lane > .v2-connect__secondary, .app.m .v2-connect .v2-connect__secondary { align-self: stretch; height: 44px; font-size: 14px; }
+
/* phone */
.mtop { flex: none; height: 52px; display: flex; align-items: center; gap: 8px; padding: 0 10px 0 12px; border-bottom: 1px solid var(--d); }
.mtop h1 { font: 800 21px/1 var(--f-display); letter-spacing: -0.03em; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@@ -659,7 +687,7 @@
Same look. Less said.
One screen first Repeated blocks become a table; rare settings collapse.
- Sidebar controls Pod chat Task board Memory Agent settings Settings Connectors Bring your own First run Dark mode
+ Sidebar controls Pod chat Task board Memory Agent settings Settings Connectors Bring your own Connect First run Dark mode
+
+
Connect /v2/connect
+
+
Class names are the page's own (v2-connect__* ), so the build copies them and its tests keep passing. The rail, page head and (i) stand in for the shared shell and V2Hint .Same family as Connectors. Lanes are bordered panels, each code block is a panel like BYO's command, and Copy is the same key.Links and acts are controls. Links sit in a 32px row at 1440 and a 44px row at 390; the agent lane's two acts are the ink key and a plain key, full width at 390.Each state, one line Member Drawn. Lanes 01 and 02.Admin Not drawn. Lane 03, Instance keys, is a third lane with one plain key.Copied Not drawn. The key's label reads Copied for 1.5s.
+
Member · 1440
157
Connect MCP uses an agent token created during setup. Commonly CLI Copy
npm install -g @commonlyai/cli
+commonly login --instance https://api.commonly.me Claude Code MCP Copy
claude mcp add commonly \
+ -e COMMONLY_API_URL=https://api.commonly.me \
+ -e COMMONLY_AGENT_TOKEN=cm_agent_… \
+ -- npx -y @commonlyai/mcp Cursor rule · save as .cursor/rules/commonly.mdc in your project Copy
---
+description: Use the team's Commonly pod for shared context and handoffs
+alwaysApply: true
+---
+
+When a task names a Commonly pod, call `commonly_get_context` with that pod's id before you start, so you have the team's current priorities and recent messages. Use `commonly_get_messages` when you need more history.
+
+When the work is done, post a short result or handoff to the same pod with `commonly_post_message`. Don't post step-by-step progress.
+
+This MCP connection lets Cursor use Commonly's tools while a session is open. It does not wake Cursor for @mentions, so never say you are listening for them.
+
+Keep agent keys out of pod messages, URLs, logs and committed files.
+ Manage connected CLI devices 02
Connect an agent you run
+
+
+
Member · 390 Connect MCP uses an agent token created during setup.
Commonly CLI Copy
npm install -g @commonlyai/cli
+commonly login --instance https://api.commonly.me Claude Code MCP Copy
claude mcp add commonly \
+ -e COMMONLY_API_URL=https://api.commonly.me \
+ -e COMMONLY_AGENT_TOKEN=cm_agent_… \
+ -- npx -y @commonlyai/mcp Cursor rule · save as .cursor/rules/commonly.mdc in your project Copy
---
+description: Use the team's Commonly pod for shared context and handoffs
+alwaysApply: true
+---
+
+When a task names a Commonly pod, call `commonly_get_context` with that pod's id before you start, so you have the team's current priorities and recent messages. Use `commonly_get_messages` when you need more history.
+
+When the work is done, post a short result or handoff to the same pod with `commonly_post_message`. Don't post step-by-step progress.
+
+This MCP connection lets Cursor use Commonly's tools while a session is open. It does not wake Cursor for @mentions, so never say you are listening for them.
+
+Keep agent keys out of pod messages, URLs, logs and committed files.
+ Manage connected CLI devices 02
Connect an agent you run Pods
157 Needs you
Team
Settings
+
+
+
First run /v2/pods/:id · Scout welcome + Guide dialog helper words welcome – → 16 dialog – → 68 scroll 390 – → 1.0×
diff --git a/docs/design/artboards/d215/targets.json b/docs/design/artboards/d215/targets.json
index 0d50b8e37..fe89f9530 100644
--- a/docs/design/artboards/d215/targets.json
+++ b/docs/design/artboards/d215/targets.json
@@ -71,6 +71,14 @@
"helperWords": 106,
"innerRatio": 1.52
},
+ "connect-1440": {
+ "helperWords": 20,
+ "innerRatio": 1.09
+ },
+ "connect-390": {
+ "helperWords": 20,
+ "innerRatio": 1.77
+ },
"firstrun-welcome-1440": {
"helperWords": 16,
"innerRatio": 1
diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
index c8ce18ebb..addf7a449 100644
--- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
+++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts
@@ -172,6 +172,8 @@ describe('v2 layout invariants (CSS rule presence)', () => {
const aprofile = read('../agents/v2-agent-profile.css');
const agentSettings = read('../agents/v2-agent-settings.css');
const byoCss = read('../components/V2AgentBYO.css');
+ // R05 lifted the Connect page onto the d215 frame (V2ConnectPage.css).
+ const connectCss = read('../components/V2ConnectPage.css');
const landing = read('../landing/v2-landing.css');
const podChat = read('../components/V2Thread.tsx');
const podsSidebar = read('../components/V2PodsSidebar.tsx');
@@ -301,8 +303,8 @@ describe('v2 layout invariants (CSS rule presence)', () => {
// #2670: the Cursor rule card's header names an unbroken save path; without
// these two declarations it pushes the Copy button out of the card at 390px.
test('Cursor rule card header wraps its save path inside the card', () => {
- expect(ruleBody(v2, '.v2-connect__rule .v2-connect__code-head span')).toContain('min-width: 0');
- expect(ruleBody(v2, '.v2-connect__rule .v2-connect__code-head code')).toContain('overflow-wrap: anywhere');
+ expect(ruleBody(connectCss, '.v2-connect .v2-connect__rule .v2-connect__code-head span')).toContain('min-width: 0');
+ expect(ruleBody(connectCss, '.v2-connect .v2-connect__rule .v2-connect__code-head code')).toContain('overflow-wrap: anywhere');
});
test('Your Team card name owns its line so the category chip cannot crush it', () => {
@@ -1437,9 +1439,11 @@ describe('v2 layout invariants (CSS rule presence)', () => {
expect(columnCount).not.toContain('border-radius:');
expect(columnCount).not.toContain('padding:');
- const connectCta = lastRuleBody(v2, '.v2-root .v2-connect__cta');
- expect(connectCta).toContain('background: var(--v2-ink)');
- expect(connectCta).toContain('color: var(--v2-on-ink)');
+ // R05: Connect's primary act is the frame's ink key, the fill gradient
+ // (--v2-fill-*) with on-fill text, the same key as BYO's submit.
+ const connectCta = lastRuleBody(connectCss, '.v2-connect .v2-connect__cta');
+ expect(connectCta).toContain('background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b))');
+ expect(connectCta).toContain('color: var(--v2-on-fill)');
});
test('the board craft-pass buttons use icon components instead of unicode glyphs', () => {
@@ -5541,6 +5545,54 @@ describe('pod board column scroll (TASK-698)', () => {
});
});
+describe('Connect lifted from the d215 frame (R05)', () => {
+ const v2 = read('../v2.css');
+ const connectCss = read('../components/V2ConnectPage.css');
+ const connect = read('../components/V2ConnectPage.tsx');
+
+ test('the page imports its own sheet and v2.css carries no Connect rule', () => {
+ expect(connect).toContain("import './V2ConnectPage.css';");
+ // `v2-connect__` and the bare root; `v2-connector*` is a different surface.
+ expect(v2).not.toMatch(/\.v2-connect(__|[\s,{:])/);
+ });
+
+ // Deviation from the frame: Copy is a 28px key, so on a phone an invisible
+ // ::after ring makes its hit area 44px. With the key's 1px border, `inset`
+ // measures from the 26px padding box, so -9px is what reaches 44.
+ test('on a phone, Copy keeps its 28px key and gains a 44px hit ring', () => {
+ const key = ruleBody(connectCss, '.v2-connect .v2-connect__copy');
+ expect(key).toContain('height: 28px');
+ expect(key).toContain('border: 1px solid var(--v2-key-edge)');
+ expect(key).toContain('position: relative');
+ const phone = mediaBlockContaining(connectCss, '.v2-connect .v2-connect__copy::after');
+ const ring = ruleBody(connectCss, '.v2-connect .v2-connect__copy::after', { within: phone });
+ expect(ring).toContain('content: ""');
+ expect(ring).toContain('position: absolute');
+ expect(ring).toContain('inset: -9px');
+ });
+
+ test('on a phone, links and the agent lane acts are 44px tall', () => {
+ const phone = mediaBlockContaining(connectCss, '.v2-connect .v2-connect__link');
+ expect(ruleBody(connectCss, '.v2-connect .v2-connect__link', { within: phone })).toContain('min-height: 44px');
+ expect(selectorRuleBody(phone, '.v2-connect .v2-connect__cta, ')).toContain('height: 44px');
+ // The same rule must size both keys, the CTA and the secondary, not the CTA alone.
+ expect(phone).toContain('.v2-connect .v2-connect__cta, .v2-connect .v2-connect__lane > .v2-connect__secondary, .v2-connect .v2-connect__secondary { align-self: stretch; height: 44px;');
+ });
+
+ // The v2.css :lang(zh) reset (TASK-055) scans only v2.css, so the entry that
+ // left it with this page is held here: every negative-tracking selector in
+ // the sheet has a zh reset in the same sheet.
+ test('zh-CN: every negative-tracking Connect title is reset under :lang(zh)', () => {
+ const negative = [...connectCss.matchAll(/\n([^\n{}]+) \{[^}]*letter-spacing:\s*-[^;]+;/g)]
+ .flatMap((m) => m[1].split(',').map((sel) => sel.trim()))
+ .filter(Boolean);
+ expect(negative).toEqual(['.v2-connect .v2-connect__lane h2']);
+ for (const sel of negative) {
+ expect(ruleBody(connectCss, `.v2-root:lang(zh) ${sel}`)).toContain('letter-spacing: 0');
+ }
+ });
+});
+
describe('chat sheets lifted out of v2.css (S3)', () => {
const chatSheets = ['chat-message', 'chat-pane', 'chat-composer', 'chat-inspector', 'chat-states']
.map((name) => read(`../chat/${name}.css`));
diff --git a/frontend/src/v2/__tests__/v2-type-floors.test.ts b/frontend/src/v2/__tests__/v2-type-floors.test.ts
index 25096dd8a..11260e233 100644
--- a/frontend/src/v2/__tests__/v2-type-floors.test.ts
+++ b/frontend/src/v2/__tests__/v2-type-floors.test.ts
@@ -26,8 +26,9 @@ import { readV2Css } from '../__testkit__/v2Css';
* longer violates). Target is an empty list.
*/
// v2.css plus the surface files lifted out of it (D223): the chat sheets V2App
-// loads, and the per-surface sheets their components import (R04 BYO, S4 board).
-const CSS = [readV2Css(), ...['../components/V2AgentBYO.css', '../components/V2PodBoard.css']
+// loads, and the per-surface sheets their components import (R04 BYO, S4 board,
+// R05 Connect).
+const CSS = [readV2Css(), ...['../components/V2AgentBYO.css', '../components/V2PodBoard.css', '../components/V2ConnectPage.css']
.map((rel) => fs.readFileSync(path.join(__dirname, rel), 'utf8'))]
.join('\n');
@@ -151,9 +152,10 @@ const ALLOWLIST: string[] = [
'.v2-runtime-row__byo', // 9.0px — no-mono-family
'.v2-runtime-row__label', // 10.5px — no-mono-family
// unassigned
+ // R05 took `.v2-connect__number` (11.0px, no family) to mono 500 11.5px
+ // with the Connect lift, so its entry left this list.
'.v2-approval__badge', // 10.5px — mono-below-floor
'.v2-approval__time', // 11.5px — no-mono-family
- '.v2-connect__number', // 11.0px — no-mono-family
'.v2-filter-count', // 11.0px — no-mono-family
'.v2-invite-card__meta', // 11.0px — no-mono-family
'.v2-invite-link', // 11.0px — no-mono-family
diff --git a/frontend/src/v2/components/V2ConnectPage.css b/frontend/src/v2/components/V2ConnectPage.css
new file mode 100644
index 000000000..2d3b81876
--- /dev/null
+++ b/frontend/src/v2/components/V2ConnectPage.css
@@ -0,0 +1,52 @@
+/* Connect (/v2/connect), lifted from the d215 frame (R05):
+ docs/design/artboards/d215/index.html, section s-connect, frame "connect" at
+ 1440 and 390. The rules below are the frame's own, same selectors and values;
+ only the tokens are translated to --v2-* (so light, dark and auto switch
+ together) and the frame's `.app.m` phone rules sit in the page's 760px phone
+ query. Scoped to .v2-connect, the page's root. */
+.v2-connect { display: flex; flex-direction: column; gap: 16px; max-width: 960px; margin: 0 auto; }
+.v2-connect .v2-connect__lane { display: flex; flex-direction: column; gap: 14px; padding: 20px; border-radius: 14px; background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border-soft); }
+.v2-connect .v2-connect__lane-heading { display: flex; align-items: baseline; gap: 10px; }
+.v2-connect .v2-connect__number { flex: none; font: 500 11.5px/1 var(--v2-font-mono); color: var(--v2-accent-text); }
+.v2-connect .v2-connect__lane h2 { margin: 0; font: 800 16px/1.25 var(--v2-font-display); letter-spacing: -0.02em; color: var(--v2-text-primary); }
+.v2-connect .v2-connect__code-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
+.v2-connect .v2-connect__code-card { min-width: 0; overflow: hidden; border-radius: 12px; background: var(--v2-bg-subtle); box-shadow: inset 0 0 0 1px var(--v2-border-soft); }
+.v2-connect .v2-connect__code-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 44px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--v2-border-soft); font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-muted); }
+.v2-connect .v2-connect__code-card pre { margin: 0; padding: 12px 14px; font: 500 12.5px/21px var(--v2-font-mono); color: var(--v2-text-primary); white-space: pre-wrap; overflow-wrap: anywhere; }
+.v2-connect .v2-connect__copy { all: unset; box-sizing: border-box; cursor: pointer; position: relative; flex: none; display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 12px; border-radius: 8px; font: 600 12.5px/1 var(--v2-font); color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); }
+.v2-connect .v2-connect__rule .v2-connect__code-head span { min-width: 0; }
+.v2-connect .v2-connect__rule .v2-connect__code-head code { font: 500 11.5px/16px var(--v2-font-mono); color: var(--v2-text-primary); overflow-wrap: anywhere; }
+.v2-connect .v2-connect__cursor { display: flex; flex-wrap: wrap; align-items: center; gap: 0 12px; font-size: 13px; line-height: 20px; color: var(--v2-text-secondary); }
+.v2-connect .v2-connect__link { display: inline-flex; align-items: center; min-height: 32px; font: 600 13px/20px var(--v2-font); color: var(--v2-accent-text); text-decoration: none; }
+.v2-connect .v2-connect__lane > .v2-connect__link, .v2-connect .v2-connect__lane > .v2-connect__secondary { align-self: flex-start; }
+.v2-connect .v2-connect__actions { display: flex; flex-wrap: wrap; gap: 8px; }
+.v2-connect .v2-connect__cta, .v2-connect .v2-connect__secondary { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; height: 34px; padding: 0 13px; border-radius: 10px; font: 600 13px/1 var(--v2-font); white-space: nowrap; text-decoration: none; cursor: pointer; }
+.v2-connect .v2-connect__cta { color: var(--v2-on-fill); background: linear-gradient(180deg, var(--v2-fill-a), var(--v2-fill-b)); border: 1px solid var(--v2-fill-edge); }
+.v2-connect .v2-connect__secondary { color: var(--v2-text-primary); background: linear-gradient(180deg, var(--v2-key-a), var(--v2-key-b)); border: 1px solid var(--v2-key-edge); box-shadow: var(--v2-key-inset); }
+@media (max-width: 760px) {
+ .v2-connect { gap: 10px; }
+ .v2-connect .v2-connect__lane { padding: 16px; }
+ .v2-connect .v2-connect__code-grid { grid-template-columns: minmax(0, 1fr); }
+ .v2-connect .v2-connect__link { min-height: 44px; }
+ .v2-connect .v2-connect__actions { flex-direction: column; }
+ .v2-connect .v2-connect__cta, .v2-connect .v2-connect__lane > .v2-connect__secondary, .v2-connect .v2-connect__secondary { align-self: stretch; height: 44px; font-size: 14px; }
+ /* Not in the frame: a 44px hit area around the 28px Copy key. With its 1px
+ border, `inset` measures from the padding box (26px), so -9px makes 44. */
+ .v2-connect .v2-connect__copy::after { content: ""; position: absolute; inset: -9px; }
+}
+
+/* Live behaviour the frame does not draw, kept working with prod's values. */
+.v2-connect .v2-connect__copy:hover,
+.v2-connect .v2-connect__secondary:hover {
+ border-color: var(--v2-border-strong);
+ background: var(--v2-surface-hover);
+ color: var(--v2-text-primary);
+}
+.v2-connect .v2-connect__cta:hover {
+ border-color: var(--v2-ink-hover);
+ background: var(--v2-ink-hover);
+}
+.v2-connect .v2-connect__link:hover { text-decoration: underline; }
+/* CJK never takes negative letter-spacing (TASK-055); this entry left the
+ v2.css :lang(zh) reset list with the rest of the page. */
+.v2-root:lang(zh) .v2-connect .v2-connect__lane h2 { letter-spacing: 0; }
diff --git a/frontend/src/v2/components/V2ConnectPage.tsx b/frontend/src/v2/components/V2ConnectPage.tsx
index 5f8abfe35..9fa883c5d 100644
--- a/frontend/src/v2/components/V2ConnectPage.tsx
+++ b/frontend/src/v2/components/V2ConnectPage.tsx
@@ -4,6 +4,7 @@ import getApiBaseUrl from '../../utils/apiBaseUrl';
import { useAuth } from '../../context/AuthContext';
import cursorMcpInstallLink from '../utils/cursorMcpInstallLink';
import { CURSOR_PROJECT_RULE, CURSOR_PROJECT_RULE_PATH } from '../utils/cursorProjectRule';
+import './V2ConnectPage.css';
type CopyKey = 'cli' | 'mcp' | 'cursorRule';
diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css
index cb371f880..1a18745b8 100644
--- a/frontend/src/v2/v2.css
+++ b/frontend/src/v2/v2.css
@@ -4854,188 +4854,6 @@ body.modern-ui.v2-canvas {
}
-/* ---------- Connect — CLI, MCP, and agent runtime entry points ---------- */
-
-.v2-connect {
- display: flex;
- flex-direction: column;
- gap: 16px;
- max-width: 960px;
- margin: 0 auto;
-}
-
-.v2-connect__lane {
- padding: 20px;
- border: 1px solid var(--v2-border);
- border-radius: var(--v2-radius-lg);
- background: var(--v2-surface);
-}
-
-.v2-connect__lane-heading {
- display: flex;
- align-items: flex-start;
- gap: 12px;
-}
-
-.v2-connect__number {
- flex: 0 0 auto;
- padding-top: 3px;
- color: var(--v2-accent-text);
- font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 11px;
- font-weight: 700;
-}
-
-.v2-connect__lane h2 {
- margin: 0;
- color: var(--v2-text-primary);
- font-size: 16px;
- font-weight: 700;
- letter-spacing: -0.01em;
-}
-
-.v2-connect__lane-heading p,
-.v2-connect__note {
- margin: 5px 0 0;
- color: var(--v2-text-secondary);
- font-size: 13px;
- line-height: 1.5;
-}
-
-.v2-connect__code-grid {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
- margin-top: 18px;
-}
-
-.v2-connect__code-card {
- overflow: hidden;
- border: 1px solid var(--v2-border-soft);
- border-radius: var(--v2-radius);
- background: var(--v2-bg-subtle);
-}
-
-.v2-connect__code-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 8px 10px;
- border-bottom: 1px solid var(--v2-border-soft);
- color: var(--v2-text-secondary);
- font-size: 12px;
- font-weight: 650;
-}
-
-.v2-connect__code-card pre {
- min-height: 92px;
- margin: 0;
- padding: 12px;
- overflow-x: auto;
- color: var(--v2-text-primary);
- font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
- font-size: 12px;
- line-height: 1.55;
- white-space: pre-wrap;
- word-break: break-word;
-}
-
-.v2-root button.v2-connect__copy,
-.v2-root .v2-connect__secondary,
-.v2-root .v2-connect__cta {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- min-height: 34px;
- padding: 7px 11px;
- border-radius: var(--v2-radius-sm);
- font-size: 13px;
- font-weight: 650;
- text-decoration: none;
- cursor: pointer;
-}
-
-.v2-root button.v2-connect__copy,
-.v2-root .v2-connect__secondary {
- border: 1px solid var(--v2-border);
- background: var(--v2-surface);
- color: var(--v2-text-secondary);
-}
-
-.v2-root button.v2-connect__copy:hover,
-.v2-root .v2-connect__secondary:hover {
- border-color: var(--v2-border-strong);
- background: var(--v2-surface-hover);
- color: var(--v2-text-primary);
-}
-
-.v2-root .v2-connect__cta {
- border: 1px solid var(--v2-ink);
- background: var(--v2-ink);
- color: var(--v2-on-ink);
-}
-
-.v2-root .v2-connect__cta:hover:not(:disabled) {
- border-color: var(--v2-ink-hover);
- background: var(--v2-ink-hover);
-}
-
-.v2-root .v2-connect__cta:disabled {
- cursor: default;
- opacity: 0.6;
-}
-
-.v2-connect__note {
- margin-top: 12px;
-}
-
-.v2-connect__note code {
- font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
-}
-
-.v2-root a.v2-connect__link {
- display: inline-block;
- margin-top: 12px;
- color: var(--v2-accent-text);
- font-size: 13px;
- font-weight: 650;
- text-decoration: none;
-}
-
-.v2-root a.v2-connect__link:hover {
- text-decoration: underline;
-}
-
-.v2-connect__cursor {
- display: flex;
- flex-wrap: wrap;
- align-items: baseline;
- gap: 4px 12px;
- margin-top: 12px;
- color: var(--v2-text-tertiary);
- font: 400 13px/20px var(--v2-font);
-}
-.v2-root .v2-connect__cursor .v2-connect__link { margin-top: 0; }
-.v2-connect__rule { margin-top: 12px; }
-.v2-connect__rule .v2-connect__code-head span { min-width: 0; }
-.v2-connect__rule .v2-connect__code-head code {
- font-family: "SF Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
- overflow-wrap: anywhere;
-}
-
-.v2-connect__actions {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- margin-top: 16px;
-}
-
-@media (max-width: 760px) {
- .v2-connect__lane { padding: 16px; }
- .v2-connect__code-grid { grid-template-columns: 1fr; }
-}
-
/* ---------- Admin · Users (waitlist + invitation codes) ---------- */
.v2-admin-users {
display: flex;
@@ -6601,8 +6419,7 @@ body.modern-ui.v2-canvas {
.v2-root:lang(zh) .v2-activity__title,
.v2-root:lang(zh) .v2-activity__section h2,
.v2-root:lang(zh) .v2-activity__agent-card h3,
-.v2-root:lang(zh) .v2-activity__board-row h3,
-.v2-root:lang(zh) .v2-connect__lane h2 {
+.v2-root:lang(zh) .v2-activity__board-row h3 {
letter-spacing: 0;
}