diff --git a/docs/design/task-122-phase-a-activity-before-after.jpg b/docs/design/task-122-phase-a-activity-before-after.jpg new file mode 100644 index 0000000000..bfa5b347d1 Binary files /dev/null and b/docs/design/task-122-phase-a-activity-before-after.jpg differ diff --git a/frontend/design-system/README.md b/frontend/design-system/README.md index c75b7d6967..2435d66a46 100644 --- a/frontend/design-system/README.md +++ b/frontend/design-system/README.md @@ -80,39 +80,41 @@ Commonly speaks to developers building with AI agents. The voice is **plainly te ### Color -- **One accent**: blue `#2f6feb`. Never blends with another. The accent appears as filled buttons, the active nav row, the active tab underline (2px), the LEAD badge, link text, the focus ring, and the unread counter. +- **One accent**: blue `#2f6feb`. Never blends with another. Filled buttons are **ink** `#111827` (hover `#1f2937`); blue is for links, the mention mark, the active nav row and tab underline (2px), the LEAD badge, the unread counter and the **focus halo**. One accent still — it no longer paints buttons (Sam, 2026-09-03, TASK-122). - **Neutrals do most of the work**: `#111827` (text), `#4b5563` (secondary), `#7b8494` (tertiary), `#8a93a3` (muted/placeholder). -- **Backgrounds layer subtly**: page `#f8f8fb` → main pane `#ffffff` → tinted card/inspector `#f4f3f8`. The shifts are tiny on purpose. +- **Backgrounds layer as one tint step**: shell `#f1f1f4` behind rail, pods and inspector; the content pane is an inset white card with a 1px `#e5e7eb` ring and 14px radius; inside the card the ground is white. `#f8f8fb` remains the page canvas behind the shell. - **Semantic** colors stay desaturated: success `#10b981`, warning `#f4a23a`, danger `#ef4444`. Info is `#0891b2` (cyan, deliberately off-axis from accent blue). Always paired with their `*-soft` background tint for badges/chips. - **Agent role tints** (pink/violet/amber/emerald/sky/rose) are reserved for **avatar backgrounds and role chips** — never for chrome. ### Type -- **SF-first stack**: `"SF Pro Text", -apple-system, BlinkMacSystemFont, "SF Pro", "Helvetica Neue", "Segoe UI", "Inter", Roboto, sans-serif`. San Francisco loads natively on Apple devices via `-apple-system` / `BlinkMacSystemFont`; non-Apple platforms fall through to Segoe UI and Inter so weights and metrics stay close. Display headings use `"SF Pro Display"` first; mono uses `"SF Mono"` first. No webfont download — system fonts for performance and OS-native feel. +- **Inter first, self-hosted**: `"Inter Variable", "Inter", "SF Pro Text", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif` via `@fontsource-variable/inter` (OFL, bundled, `font-display: swap`, no Google Fonts request). SF Pro is the fallback only while the woff2 loads or if it fails. One face: display weight comes from `font-weight`, not a second family. Mono keeps `"SF Mono"` first. - **Tight letter-spacing on big text**: feature titles `-0.03em`, h2 `-0.025em`, h3 `-0.02em`. Body stays at 0. -- **Heavy weights at large sizes**: feature titles use `850`, headers `700`, section titles `650`. Body is `400`/`500`. Buttons are `600`. -- **Sizes hover small**: body `14px`, secondary `13px`, meta `12px`, kicker `10–11px`. The biggest type in normal product UI is the 24px feature title. +- **Heavy weights at large sizes**: feature titles use `700` in Inter (`850` was tuned for SF Pro Display), headers `700`, section titles `650`. Body is `400`/`500`. Buttons are `600`. +- **Sizes hover small**: body `14/20`, meta `12/16`, labels `11/14`. Secondary copy keeps body size and carries hierarchy by colour. The biggest type in normal product UI is the 24px feature title. ### Spacing, radii, layout - Spacing follows a 4px grid: `4 / 8 / 16 / 24 / 32 / 48`. -- Radii cluster around three sizes: `8` (chips/small buttons), `10` (default), `12` (cards/modals). `999` for pills. +- Radii ladder `6 / 10 / 14` by importance: `6` chips, inputs and small buttons; `10` cards, rows and buttons ≥ 36px; `14` the content card, modals and the login card. `999` for pills. - The shell is a **fixed 4-column grid**: `76px rail · 272px pods · 1fr main · 336px inspector`. The inspector collapses to a 3-col layout on feature pages and defaults to collapsed. ### Backgrounds -- **No gradients in chrome.** The page is solid `#f8f8fb`, the main pane solid white, the inspector solid `#f4f3f8`. **Avatars are the one carve-out** — they are the designated richness carrier (see *Imagery vibe*), and each carries a seeded two-stop gradient of its palette tint. Chrome surfaces stay flat. +- **The tint step.** Shell `#f1f1f4` behind rail, pods and inspector; content is an inset white card (1px `#e5e7eb` ring, 14px radius, 8px gutter top/right/bottom on desktop, flush on phones). +- **No gradients in chrome.** The shell is solid `#f1f1f4`, the content card solid white. **Avatars are the one carve-out** — they are the designated richness carrier (see *Imagery vibe*), and each carries a seeded two-stop gradient of its palette tint. Chrome surfaces stay flat. - **No background images, illustrations, or patterns.** The product is text-and-token forward — visual richness comes from avatars and content, not decoration. ### Borders & elevation -- **Borders, not shadows.** Cards are `1px solid #e5e7eb`. Hairlines between sections use `#eef0f6`. Hovered borders deepen to `#d7dce7`. -- The v2 token explicitly sets `--v2-shadow: none` and `--v2-shadow-sm: none`. **Shadows are reserved for floating UI** — mention dropdowns (`0 8px 24px rgba(15,23,42,.12)`), login card, dialogs. +- **Borders, not shadows.** Cards are `1px solid #e5e7eb`. Hairlines between sections use `#eef0f6`. Hovered borders deepen to `#d7dce7`. Rows carry a **permanent transparent 1px border** and change only their fill on hover; no dividers between rows. +- The v2 token explicitly sets `--v2-shadow: none` and `--v2-shadow-sm: none`. **Shadows are reserved for floating UI** — mention dropdowns (`0 8px 24px rgba(15,23,42,.12)`), login card, dialogs. The single shadow allowed on a chrome card is `0 1px 2px rgba(15,23,42,.06)` on a **pending** decision or approval card; it drops when the card settles. - **No "inner shadow" / inset effects** on chrome surfaces. Avatars again excepted: the initials plate carries a 1px inset highlight/shade pair so the seeded gradient reads as a lit sphere rather than a flat disc — removed via `:has(img)` when a photo is present, so shading never sits on a face. ### Hover & press -- **Hover** = swap to a slightly darker neutral background (`--c-surface-hover: #f1f2f5`) or a tinted accent (`#e8efff`). Borders may strengthen one tier. +- **Hover** = fill swap only (`--c-surface-hover: #f1f2f5` or the accent tint `#e8efff`). Nothing shifts by a pixel: no border-colour, padding, transform or shadow change on hover. +- **Focus** = halo `0 0 0 3px rgba(47,111,235,.18)` with an accent edge; never a hard 2px outline (forced-colors mode gets the UA outline). - **Press** = no `transform: translateY` in V2. - **Active state** for tabs/nav uses the accent bottom-border (2px) or the accent-soft pill background — not bold weight changes. - **Transitions**: very fast — `80ms ease` for hover/state changes is the V2 default. `120ms` for card hovers, `300ms` for layout shifts. @@ -171,7 +173,7 @@ entrance and scroll-reveal animation is allowed within these limits: ### Layout rules -- **Fixed sidebar widths** (rail 76, pods 272, inspector 336) — not fluid. The main pane absorbs all flex. +- **Fixed sidebar widths** (rail 76, pods 272, inspector 336) — not fluid. The main pane absorbs all flex. The content pane is the card: 8px gutter top/right/bottom on desktop, flush on phones; when the inspector is open the card meets the inspector gutter. - **Rail collapses labels** at all widths — labels show as tooltips on hover (CSS `::after` with `data-label`). - **Inspector closes** to free width for the main pane; default state is collapsed. - Header heights cluster at **42px (tabs)**, **34px (chips/buttons)**, **78px (chat header w/ subtitle)**. @@ -208,6 +210,6 @@ entrance and scroll-reveal animation is allowed within these limits: ## Caveats -- **No webfonts shipped.** V2 deliberately uses system fonts. If a design needs Inter explicitly, load it from Google Fonts — but match the V2 metrics by setting `font-family` accordingly. +- **One webfont shipped.** Inter Variable is bundled from `@fontsource-variable/inter` (OFL); everything else is system. Do not add a second family, and never load a face from Google Fonts. - **No Figma file** is committed. All visual decisions trace back to `tokens.css` and `frontend/src/v2/v2.css`. - **Marketing site styling lives elsewhere.** Only the in-app experience is documented here. diff --git a/frontend/design-system/tokens.css b/frontend/design-system/tokens.css index 48e49fbdf7..f16f626c34 100644 --- a/frontend/design-system/tokens.css +++ b/frontend/design-system/tokens.css @@ -24,6 +24,8 @@ --c-surface: #ffffff; --c-surface-tint: #f4f3f8; /* inspector, code blocks */ --c-surface-hover: #f1f2f5; + --c-shell-bg: #f1f1f4; /* shell ground: rail, pods, inspector, gutter around the content card (TASK-122) */ + --c-content-radius: 14px; /* the inset content card */ /* ---------- Borders ---------- */ --c-border: #e5e7eb; /* default */ @@ -36,6 +38,9 @@ --c-fg-tertiary: #7b8494; --c-fg-muted: #8a93a3; --c-fg-on-accent: #ffffff; + --c-ink: #111827; /* filled buttons are ink; blue is links, marks, halo (TASK-122) */ + --c-ink-hover: #1f2937; + --c-on-ink: #ffffff; /* ---------- Semantic ---------- */ --c-success: #10b981; @@ -66,9 +71,9 @@ --c-github: #333333; /* ---------- Geometry ---------- */ - --c-radius-sm: 8px; /* chips, small buttons */ + --c-radius-sm: 6px; /* chips, inputs, small buttons */ --c-radius: 10px; /* default — inputs, cards inside panels */ - --c-radius-lg: 12px; /* cards, modals */ + --c-radius-lg: 14px; /* the content card, modals, the login card */ --c-radius-pill: 999px; /* ---------- Spacing scale ---------- */ @@ -91,6 +96,7 @@ --c-shadow-sm: 0 1px 2px 0 rgba(15, 23, 42, 0.04); --c-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06); --c-shadow-lg: 0 8px 24px rgba(15, 23, 42, 0.12); + --c-shadow-pending: 0 1px 2px rgba(15, 23, 42, 0.06); /* the one chrome-card shadow: a PENDING decision/approval only */ /* ---------- Motion ---------- */ --c-ease: cubic-bezier(0.4, 0, 0.2, 1); @@ -111,15 +117,12 @@ --motion-ease-linear: linear; /* ---------- Typography ---------- */ - /* SF-first stack — uses the OS-installed San Francisco family on Apple - devices; falls through to BlinkMacSystemFont (Chrome's SF alias) and - finally to Segoe/Inter on non-Apple platforms. */ - --c-font-sans: "SF Pro Text", -apple-system, BlinkMacSystemFont, - "SF Pro", "Helvetica Neue", "Segoe UI", "Inter", - Roboto, sans-serif; - --c-font-display: "SF Pro Display", -apple-system, BlinkMacSystemFont, - "SF Pro", "Helvetica Neue", "Segoe UI", "Inter", - sans-serif; + /* Inter (variable, self-hosted from the bundle via @fontsource-variable/inter); + SF Pro is the fallback only while the woff2 loads or if it fails. One face: + display weight comes from font-weight, not a second family. (TASK-122) */ + --c-font-sans: "Inter Variable", "Inter", "SF Pro Text", -apple-system, + BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --c-font-display: var(--c-font-sans); --c-font-mono: "SF Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, "JetBrains Mono", "Fira Code", monospace; @@ -145,6 +148,9 @@ --c-lh-tight: 1.25; --c-lh-base: 1.45; --c-lh-comfy: 1.55; + --c-lh-body: 20px; /* 14/20 body, 12/16 meta, 11/14 labels — mirror --v2-lh-* */ + --c-lh-meta: 16px; + --c-lh-label: 14px; /* Platform brand + tint tokens (connectors) — mirror of --v2-platform-* in src/v2/v2.css; the two move together. */ --c-platform-telegram: #2aabee; @@ -184,7 +190,7 @@ html, body { h1, .c-h1 { font-family: var(--c-font-sans); font-size: var(--c-fs-3xl); - font-weight: 850; /* V2 uses 850 for feature titles */ + font-weight: 700; /* 850 was tuned for SF Pro Display; Inter reads right at 700 */ line-height: var(--c-lh-tight); letter-spacing: -0.03em; margin: 0; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2b981932d3..3a47fe5b7c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@dnd-kit/sortable": "^8.0.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", + "@fontsource-variable/inter": "^5.3.0", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.1", "@sentry/react": "^10.65.0", @@ -3183,6 +3184,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index cbdaf8076a..44c072e775 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "@dnd-kit/sortable": "^8.0.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", + "@fontsource-variable/inter": "^5.3.0", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.1", "@sentry/react": "^10.65.0", @@ -85,6 +86,7 @@ "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" }, "moduleNameMapper": { + "^@fontsource-variable/inter$": "/src/__mocks__/fileMock.js", "^react-markdown$": "/src/__mocks__/react-markdown.js", "^d3$": "/src/__mocks__/d3.js", "\\.(css|less|scss|sass)$": "/src/__mocks__/fileMock.js", diff --git a/frontend/src/v2/V2App.tsx b/frontend/src/v2/V2App.tsx index 349b6e2f3b..5b47e89376 100644 --- a/frontend/src/v2/V2App.tsx +++ b/frontend/src/v2/V2App.tsx @@ -43,6 +43,7 @@ import V2AdminUsers from './components/V2AdminUsers'; import V2AdminAnalytics from './components/V2AdminAnalytics'; import V2EmailVerificationBanner from './components/V2EmailVerificationBanner'; import ProtectedRoute from '../components/ProtectedRoute'; +import '@fontsource-variable/inter'; import './v2.css'; class V2ErrorBoundary extends React.Component<{ children: React.ReactNode }, { error: Error | null }> { diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index e78d80526e..897805810f 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -53,6 +53,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { const podChat = read('../components/V2PodChat.tsx'); const podBoard = read('../components/V2PodBoard.tsx'); const yourTeam = read('../components/V2YourTeamPage.tsx'); + const activityPage = read('../components/V2ActivityPage.tsx'); test('Your Team card name owns its line so the category chip cannot crush it', () => { const rule = ruleBody(v2, '.v2-team-card__name'); @@ -182,8 +183,8 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(neutralOption).toContain('border-radius: 999px'); const recommendedOption = ruleBody(v2, '.v2-root .v2-activity__queue-actions button.v2-activity__option--recommended'); - expect(recommendedOption).toContain('background: var(--v2-accent)'); - expect(recommendedOption).toContain('color: var(--v2-surface)'); + expect(recommendedOption).toContain('background: var(--v2-ink)'); + expect(recommendedOption).toContain('color: var(--v2-on-ink)'); expect(v2).toContain('.v2-activity__option-description'); }); @@ -566,7 +567,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { }); test('Activity queue actions distinguish an action from the thread handoff', () => { - expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button')).toContain('background: var(--v2-accent)'); + expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button')).toContain('background: var(--v2-ink)'); expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button.v2-activity__queue-action--secondary')).toContain('background: var(--v2-surface-hover)'); expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button.v2-activity__queue-action--thread')).toContain('background: transparent'); }); @@ -576,7 +577,7 @@ describe('v2 layout invariants (CSS rule presence)', () => { // recommended state and the free-text escape hatch visible in the CSS // source because jsdom has no layout engine to catch a narrow regression. expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button.v2-activity__option--recommended')) - .toContain('background: var(--v2-accent)'); + .toContain('background: var(--v2-ink)'); expect(ruleBody(v2, '.v2-activity__decision-other')).toContain('flex-basis: 100%'); expect(v2).toMatch(/@media \(max-width: 640px\)[\s\S]*?\.v2-root \.v2-activity__queue-actions button \{ min-height: 44px; \}/); }); @@ -887,4 +888,89 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(v2).toContain('var(--v2-platform-telegram-soft)'); }); + describe('TASK-122 Phase A — the ruled restyle (Sam, 2026-09-03; spec on TASK-122)', () => { + const v2Root = ruleBody(v2, '.v2-root'); + + test('the tint step exists in both token files and the content pane is the inset card', () => { + expect(cssVariable(v2Root, '--v2-shell-bg')).toBe('#f1f1f4'); + expect(cssVariable(tokens, '--c-shell-bg')).toBe('#f1f1f4'); + expect(cssVariable(v2Root, '--v2-content-radius')).toBe('14px'); + expect(cssVariable(tokens, '--c-content-radius')).toBe('14px'); + expect(ruleBody(v2, '.v2-shell')).toContain('var(--v2-shell-bg)'); + const main = ruleBody(v2, '.v2-pane--main'); + expect(main).toContain('border: 1px solid var(--v2-border)'); + expect(main).toContain('border-radius: var(--v2-content-radius)'); + // The banner variant must out-specify `.v2-authenticated-shell__content .v2-pane { height: 100% }`. + expect(ruleBody(v2, '.v2-authenticated-shell .v2-pane--main')).toContain('height: calc(100% - 16px)'); + // Phones: the card goes flush — both selectors, or the banner variant keeps the desktop height. + expect(v2).toMatch(/@media \(max-width: 760px\) \{[\s\S]*?\.v2-pane--main,\n\s*\.v2-authenticated-shell \.v2-pane--main \{[\s\S]*?border-radius: 0/); + expect(ruleBody(v2, '.v2-feature')).toContain('background: #ffffff'); + }); + + test('the radius ladder is 6 / 10 / 14 in both files', () => { + for (const [v2Name, dsName, value] of [ + ['--v2-radius-sm', '--c-radius-sm', '6px'], + ['--v2-radius', '--c-radius', '10px'], + ['--v2-radius-lg', '--c-radius-lg', '14px'], + ]) { + expect(cssVariable(v2Root, v2Name)).toBe(value); + expect(cssVariable(tokens, dsName)).toBe(value); + } + }); + + test('Needs-you rows are stable on hover: transparent border, fill-only hover, no dividers', () => { + const row = ruleBody(v2, '.v2-activity__queue-row'); + expect(row).toContain('border: 1px solid transparent'); + expect(row).toContain('border-radius: var(--v2-radius)'); + expect(v2).not.toContain('.v2-activity__queue-row + .v2-activity__queue-row'); + const hover = ruleBody(v2, '.v2-activity__queue-row:hover'); + const declarations = hover.slice(hover.indexOf('{') + 1).split(';').map((d) => d.trim()).filter(Boolean); + expect(declarations.length).toBeGreaterThan(0); + for (const declaration of declarations) expect(declaration).toMatch(/^background/); + // Pending decision/approval rows sit one step up; a ruled decision settles back down. + const pending = ruleBody(v2, '.v2-activity__queue-row--decision,\n.v2-activity__queue-row--approval'); + expect(pending).toContain('box-shadow: var(--v2-shadow-pending)'); + expect(cssVariable(v2Root, '--v2-shadow-pending')).toBe(cssVariable(tokens, '--c-shadow-pending')); + expect(ruleBody(v2, '.v2-activity__queue-row--settled')).toContain('box-shadow: none'); + expect(activityPage).toContain("' v2-activity__queue-row--settled'"); + }); + + test('halo focus: no hard outline in any Activity focus-visible rule; the global halo still carries the ring', () => { + const activityFocusRules = v2.split('}').filter((block) => { + const brace = block.indexOf('{'); + if (brace === -1) return false; + const selector = block.slice(0, brace); + return selector.includes('.v2-activity__') && selector.includes(':focus-visible'); + }); + expect(activityFocusRules.length).toBeGreaterThan(0); + for (const block of activityFocusRules) expect(block.slice(block.indexOf('{'))).not.toContain('outline: 2px'); + expect(ruleBody(v2, '.v2-root button:focus-visible,\n.v2-root input:focus-visible,\n.v2-root textarea:focus-visible,\n.v2-root a:focus-visible')).toContain('box-shadow: var(--v2-focus-ring)'); + // is outside + that global list, so the pod picker carries its own halo. No hard outline. */ +.v2-root .v2-activity__compose textarea:focus-visible { + border-color: var(--v2-accent); +} + .v2-activity__compose-pod select:focus-visible { - outline: 2px solid var(--v2-accent); - outline-offset: 1px; + outline: none; + box-shadow: var(--v2-focus-ring); + border-color: var(--v2-accent); } .v2-root .v2-activity__compose button { min-height: 44px; - padding: 0 15px; - border: 1px solid var(--v2-accent); - border-radius: var(--v2-radius-sm); - background: var(--v2-accent); - color: var(--v2-surface); + padding: 0 16px; + border: 1px solid var(--v2-ink); + border-radius: var(--v2-radius); + background: var(--v2-ink); + color: var(--v2-on-ink); font-size: 13px; - font-weight: 700; + font-weight: 600; } .v2-root .v2-activity__compose button:hover:not(:disabled) { - border-color: var(--v2-accent-strong); - background: var(--v2-accent-strong); + border-color: var(--v2-ink-hover); + background: var(--v2-ink-hover); } .v2-activity__section-heading { @@ -8652,14 +8710,20 @@ body.modern-ui.v2-canvas { letter-spacing: -0.02em; } -.v2-activity__queue, .v2-activity__board-list { border: 1px solid var(--v2-border-soft); border-radius: var(--v2-radius); overflow: hidden; } -.v2-activity__queue-row, +/* Needs-you rows: no box, no dividers (Sam ruled 2026-09-03, TASK-122). Each + row carries a permanent transparent 1px border and changes only its fill on + hover, so nothing shifts by a pixel. Board rows keep their box until Phase B. */ +.v2-activity__queue { + display: grid; + gap: 2px; +} + .v2-activity__board-row { display: grid; align-items: center; @@ -8669,13 +8733,51 @@ body.modern-ui.v2-canvas { background: var(--v2-surface); } -.v2-activity__queue-row + .v2-activity__queue-row, .v2-activity__board-row + .v2-activity__board-row { border-top: 1px solid var(--v2-border-soft); } .v2-activity__queue-row { + display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; + align-items: center; + gap: 14px; + min-width: 0; + padding: 12px 14px; + border: 1px solid transparent; + border-radius: var(--v2-radius); + background: transparent; + transition: background 80ms ease; +} + +.v2-activity__queue-row:hover { + background: var(--v2-surface-hover); +} + +/* Decision elevation: a pending decision or approval sits one step above the + settled rows — the one shadow allowed on a chrome card, and only while + pending. Hover does not move it; it is already up. */ +.v2-activity__queue-row--decision, +.v2-activity__queue-row--approval { + background: var(--v2-surface); + border-color: var(--v2-border); + box-shadow: var(--v2-shadow-pending); +} + +.v2-activity__queue-row--decision:hover, +.v2-activity__queue-row--approval:hover { + background: var(--v2-surface); +} + +/* A ruled decision has settled: it drops back to the flat row. */ +.v2-activity__queue-row--settled { + background: transparent; + border-color: transparent; + box-shadow: none; +} + +.v2-activity__queue-row--settled:hover { + background: var(--v2-surface-hover); } .v2-activity__queue-mark { @@ -8714,7 +8816,8 @@ body.modern-ui.v2-canvas { .v2-activity__task-id, .v2-activity__update-time { color: var(--v2-text-muted); - font-size: 12px; + font-size: var(--v2-fs-meta); + line-height: var(--v2-lh-meta); } .v2-activity__queue-copy strong, @@ -8729,15 +8832,16 @@ body.modern-ui.v2-canvas { display: block; margin-top: 2px; color: var(--v2-text-primary); - font-size: 14px; + font-size: var(--v2-fs-body); + line-height: var(--v2-lh-body); } .v2-activity__queue-copy p, .v2-activity__board-row p { margin: 3px 0; color: var(--v2-text-secondary); - font-size: 13px; - line-height: 1.5; + font-size: var(--v2-fs-body); + line-height: var(--v2-lh-body); } .v2-activity__queue-actions, @@ -8760,15 +8864,15 @@ body.modern-ui.v2-canvas { } .v2-root .v2-activity__queue-actions button { - border-color: var(--v2-accent); - background: var(--v2-accent); - color: var(--v2-surface); + border-color: var(--v2-ink); + background: var(--v2-ink); + color: var(--v2-on-ink); transition: background 80ms ease, border-color 80ms ease, color 80ms ease; } .v2-root .v2-activity__queue-actions button:hover:not(:disabled) { - border-color: var(--v2-accent-strong); - background: var(--v2-accent-strong); + border-color: var(--v2-ink-hover); + background: var(--v2-ink-hover); } .v2-root .v2-activity__queue-actions button.v2-activity__queue-action--secondary { @@ -8814,9 +8918,9 @@ body.modern-ui.v2-canvas { } .v2-root .v2-activity__queue-actions button.v2-activity__option--recommended { - border-color: var(--v2-accent); - background: var(--v2-accent); - color: var(--v2-surface); + border-color: var(--v2-ink); + background: var(--v2-ink); + color: var(--v2-on-ink); } .v2-root .v2-activity__queue-actions button.v2-activity__option.v2-activity__queue-action--secondary { @@ -8889,7 +8993,7 @@ body.modern-ui.v2-canvas { .v2-activity__agent-card { padding: 16px; - border: 1px solid var(--v2-border-soft); + border: 1px solid var(--v2-border); border-radius: var(--v2-radius); background: var(--v2-surface); } @@ -9388,18 +9492,17 @@ body.modern-ui.v2-canvas { flex: 1 1 auto; min-width: 0; resize: vertical; - padding: 6px 8px; + padding: 8px 10px; font: inherit; - font-size: 13px; - line-height: 1.4; - color: var(--v2-text); + font-size: var(--v2-fs-body); + line-height: var(--v2-lh-body); + color: var(--v2-text-primary); background: var(--v2-surface); border: 1px solid var(--v2-border); - border-radius: var(--v2-radius); + border-radius: var(--v2-radius-sm); } .v2-root textarea.v2-activity__reply-input:focus-visible { - outline: 2px solid var(--v2-accent); - outline-offset: 1px; + border-color: var(--v2-accent); /* the global halo carries the ring; no hard outline */ } @media (max-width: 640px) { .v2-activity__reply { flex-basis: 100%; }