diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 000000000..97e663b17 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1 @@ +.astro/ diff --git a/website/README.md b/website/README.md index d5f4e5824..f77bba01f 100644 --- a/website/README.md +++ b/website/README.md @@ -7,7 +7,7 @@ End-user documentation site for **Scope**, built with and published to GitHub Pages. The product and this documentation site live in -[scope-core](https://github.com/growth-ecosystems/scope-core). +[microsoft/scope](https://github.com/microsoft/scope). ## Project structure @@ -16,7 +16,8 @@ The product and this documentation site live in ├── public/ # static assets ├── src/ │ ├── assets/ -│ ├── components/community/ # article/talk lists + landing teaser +│ ├── components/ # Astro landing, interactive example, header, page title +│ │ └── community/ # article/talk lists + landing teaser │ ├── content/ │ │ ├── docs/ # all user-facing pages (.md / .mdx) │ │ │ ├── introduction/ @@ -31,6 +32,8 @@ The product and this documentation site live in │ ├── plugins/ │ │ ├── remark-base-path.mjs # applies the deployment base to internal links │ │ └── remark-http-snippets.mjs # turns ```http blocks into multi-language tabs +│ ├── scripts/flow-demo.ts # progressive-enhancement example controller +│ ├── styles/landing.css # shared brand tokens + scoped landing styles │ └── content.config.ts ├── astro.config.mjs # sidebar, plugins, starlight-openapi config ├── AGENTS.md # conventions & guardrails for contributors / AI agents @@ -45,7 +48,7 @@ Sidebar order is defined in `astro.config.mjs`, not by directory order. | Command | Action | | :--------------------- | :--------------------------------------------------------- | | `pnpm install` | Install dependencies | -| `pnpm dev` | Start local dev server at `localhost:4321` | +| `pnpm dev` | Start dev server at the worktree's `DOC_PORT` (fallback: 4300) | | `pnpm build` | Build the production site to `./dist/` | | `pnpm preview` | Preview the production build locally | | `pnpm test` | Test site plugins with Node's built-in test runner | @@ -87,7 +90,7 @@ where to look in scope-core for any given topic. ## Deployment -Pushed builds deploy to GitHub Pages via +Changes merged to `main` deploy to GitHub Pages via [../.github/workflows/static.yml](../.github/workflows/static.yml). The workflow builds from this `website/` directory (via a `working-directory` default and `website/**` path filters). Both pull-request @@ -107,8 +110,169 @@ SITE=https://microsoft.github.io BASE_PATH=/scope pnpm build SITE=https://microsoft.github.io BASE_PATH=/scope pnpm preview ``` + Open `/scope/` on the preview server. Keep `BASE_PATH` the same for the build and preview so assets, navigation, and search use the same URLs. +Use the production preview to exercise Pagefind search; its index is +generated at build time. + +Author Markdown links as `/getting-started/access/`, for example. +The base-path remark plugin prefixes Markdown links, reference +definitions, images, and literal MDX `href`/`src` attributes. +Astro components must construct internal URLs with +`import.meta.env.BASE_URL`. External URLs, protocol-relative URLs, +fragments, and relative links are left unchanged. The generated API +landing route is `/reference/api/`, not `/reference/api/operations/`. + +## Experience design + +The landing page is a product introduction and an entry point into the +documentation, not a replacement for the documentation reader. +Starlight retains search, theme selection, the mobile sidebar, the +table of contents, code examples, and previous/next navigation. + +- **Visual language:** restrained indigo, neutral surfaces, readable + system fonts, a dotted flow canvas, and shared light/dark tokens. + Green and red communicate criterion outcomes, always with text. +- **Navigation:** a landing header with direct documentation and demo + links, plus task-oriented guide groups: run experiments, design a + benchmark, and connect tools. Existing document URLs are preserved. +- **Onboarding:** a concrete example before terminology, followed by + separate Portal, CLI, and API entry points. Access requirements are + explicit rather than promising instant access to a hosted service. +- **Motion:** limited to the silent showreel and the interactive + example, with pause controls and reduced-motion support. The logo + and documentation chrome remain still. + +### Showreel + +The supplied Scope showreel plays in a framed 16:9 player below the +hero calls to action, so the headline stays clean and the footage is +shown without an overlay. The local [video](public/scope-showreel.mp4) +is re-encoded as H.264 at half the original speed (30 seconds instead +of 15), with the audio track removed and metadata moved to the front +for web playback. It loops, is always muted, and plays inline on +mobile. A visually hidden caption describes the scenes it shows. + +[showreel.ts](src/scripts/showreel.ts) starts playback when at least a +quarter of the player is visible. A keyboard-accessible button pauses +or resumes it. Scrolling it out of view or hiding the tab pauses +playback; an explicit user pause persists when returning. The button +overlays the bottom corner of the video, and moves below it on narrow +screens so it does not cover the footage. + +Reduced-motion visitors see the [poster](public/scope-showreel-poster.jpg), +taken from the closing Scope title card, without downloading the video +until they choose to play it. Without JavaScript, the poster remains +visible and the playback button stays hidden. Blocked autoplay offers +manual playback; media failures display a status message and log the +error. Both media URLs use the configured deployment base. + +### Interactive example + +[FlowDemo.astro](src/components/FlowDemo.astro) renders the full diagram +and an initial task prompt as static HTML. +[flow-demo.ts](src/scripts/flow-demo.ts) progressively enhances it using +a custom element. It makes no network requests and adds no framework +or animation-library dependencies. + +One task-board example illustrates task submission, a base profile with +two alternate profiles, evaluation gates, and per-criterion evidence. +There is no scenario picker: the task and criteria stay fixed throughout +the walkthrough. Outcomes, token counts, and durations are labeled as +simulations, not measurements or agent rankings. + +The heading and navigation use **Interactive example**, with a neutral +**Mock data** badge and **No agents run here** explanation at the top. +There is no glowing status indicator, and profile cards say **Example +profile**, not "Ready", "Working", or "Complete". Playback controls +explicitly describe an animation, not an agent execution. + +The single example is shared by the static diagram and the controller. +The diagram's result bars reflect each profile's passed-gate count, and +the results table uses the same fixture. There is no scenario-switching +state or change-summary panel. + +The availability strip identifies GitHub Copilot and Claude Code as +**Supported today**, OpenAI Codex and OpenCode as **Planned**, and Cursor +as **Not integrated**. Planned agents have no promised release date and +do not appear in the simulated executions. + +[sample-agents.ts](src/scripts/sample-agents.ts) separates availability +from the fictional profiles. The base uses Copilot, Var 1 adds a task +skill with the same agent and model, and Var 2 changes the agent and +model to Claude Code. All use the same task and criteria. Connector +branches and profile counts derive from this shared profile list. + +The judge step distinguishes **gates** from the reusable **criteria +library**. Requirements, Build, and Test are sequential gates. An +optional **Explore the criteria graph** disclosure shows the graph +inside each gate, including two independent checks that depend on +"Tests execute". The graph is collapsed by default to keep the +homepage introduction approachable. + +[flow-demo-data.ts](src/scripts/flow-demo-data.ts) owns the invented +fixtures and derives consistent gate and criterion outcomes: failing +a gate skips later gates, while failing a criterion skips its +descendants, not its siblings. The results table compares each +variation with the base, showing gates passed, input/output and total +tokens, elapsed run duration, and signed token/time differences. +Duration describes the fictional run, not the animation. Lower usage +or shorter duration is not presented as a win when the run failed. +Per-criterion outcomes remain available in a separate disclosure. + +Playback runs once when the example enters view. Users can pause, +reset, replay, or inspect any step with a button. +Leaving the viewport or hiding the tab stops playback. Reduced-motion +users get manual **Next step** controls and no animated connectors. +Without JavaScript, the static diagram and explanation remain visible, +and nonfunctional playback controls stay hidden. + +The example data tests use the monorepo's existing Vitest runner. +After installing root and website dependencies, run from the repo root: + +```sh +pnpm exec vitest run --config website/vitest.config.ts +``` + +The website has its own test configuration because its standalone +Astro dependencies are not part of the root pnpm workspace install. + +### Review findings and recommendations + +The September 2026 review covered all 29 handwritten pages, navigation +into the generated API reference, onboarding, landing design, motion, +themes, responsive layout, and GitHub Pages paths. It was a UX and +documentation review, not an endpoint-by-endpoint API correctness audit. + +| Finding in the previous site | Change | +| --- | --- | +| Abstract hero and repetitive feature sections made the value hard to grasp. | Concrete product framing, an explorable example, and three evidence-focused cards. | +| Static agent scores looked like real comparative claims and did not explain the flow. | Clearly fictional scenarios with inspectable stages and dependency-aware outcomes. | +| A flat list of 14 guides mixed onboarding, configuration, and advanced integrations. | Task-oriented groups with advanced sections collapsed initially. | +| Hard-coded root links escaped a GitHub Pages project base. | Base-aware Astro links, a Markdown/MDX transform, and project-path PR builds. | +| Seven API-reference links pointed to a nonexistent operations index. | Links now target the generated API landing page. | +| The custom hero hid Starlight's skip-link destination. | One visible heading with the native `_top` anchor and a homepage-only title override. | +| The glossary described an optional DAG and included copy errors. | DAG-only terminology, corrected prose, and capability cross-links. | +| The custom install-directory example set the variable on the downloader, not the installer. | Apply `SCOPE_INSTALL_DIR` to the receiving `bash` process. | + +Recommended content follow-ups: + +- Reconcile the VS Code capability matrix with the documented worker + ID policy. The introduction and reference currently describe three + agents, while the worker ID list names only two. The landing's + supported-agent strip names only the two documented ACP workers; + the interactive example separates them from planned or unavailable + integrations and runs fictional profiles of supported agents only. +- Walk the first-run guide against a current deployment, then add + maintained screenshots. Its claims about preseeded catalogs, model + availability, and UI labels should not be assumed for every deployment. +- Explain release-repository access during CLI onboarding. The legacy + `growth-ecosystems/scope-doc` reference is still used by the installer + and publishing workflow, so changing it just because the documentation + moved would be incorrect. A release migration is a separate change. +- Add a real, reproducible sample-results walkthrough when an approved + dataset is available. Keep real evidence separate from the example. ## Learn more diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 3f662e009..c637dd8c7 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -53,6 +53,7 @@ const base = process.env.BASE_PATH || '/'; export default defineConfig({ site: process.env.SITE || `http://localhost:${docPort}`, base, + output: 'static', server: { port: docPort }, markdown: { remarkPlugins: [[remarkBasePath, { base }], remarkHttpSnippets], @@ -68,7 +69,10 @@ export default defineConfig({ customCss: ['./src/styles/landing.css'], components: { SiteTitle: './src/components/SiteTitle.astro', + Header: './src/components/Header.astro', + PageTitle: './src/components/PageTitle.astro', }, + social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/microsoft/scope' }], plugins: [ starlightOpenAPI([ { @@ -101,24 +105,42 @@ export default defineConfig({ { label: 'Guides', items: [ - { label: 'Submitting requests (Portal)', slug: 'guides/submitting-requests-portal' }, - { label: 'Submitting requests (REST API)', slug: 'guides/submitting-requests-api' }, - { label: 'Submitting requests (CLI)', slug: 'guides/submitting-requests-cli' }, - { label: 'Managing task prompts', slug: 'guides/managing-task-prompts' }, - { label: 'Defining evaluation criteria', slug: 'guides/defining-criteria' }, - { label: 'Defining profiles', slug: 'guides/defining-profiles' }, - { label: 'Working with prompt features', slug: 'guides/prompt-features' }, - { label: 'Choosing a coding agent', slug: 'guides/choosing-a-coding-agent' }, - { label: 'Choosing software stacks', slug: 'guides/software-stacks' }, - { label: 'Prioritizing & pausing requests', slug: 'guides/prioritizing-requests' }, - { label: 'Importing MCP servers', slug: 'guides/importing-mcp-servers' }, - { label: 'Importing skills', slug: 'guides/importing-skills' }, - { label: 'Importing VS Code extensions', slug: 'guides/importing-extensions' }, - { label: 'Using MCP servers, skills & extensions', slug: 'guides/mcp-skills-extensions' }, + { + label: 'Run experiments', + items: [ + { label: 'Submit from the Portal', slug: 'guides/submitting-requests-portal' }, + { label: 'Submit from the CLI', slug: 'guides/submitting-requests-cli' }, + { label: 'Submit through the API', slug: 'guides/submitting-requests-api' }, + { label: 'Prioritize & pause', slug: 'guides/prioritizing-requests' }, + ], + }, + { + label: 'Design your benchmark', + collapsed: true, + items: [ + { label: 'Task prompts', slug: 'guides/managing-task-prompts' }, + { label: 'Evaluation criteria', slug: 'guides/defining-criteria' }, + { label: 'Agent profiles', slug: 'guides/defining-profiles' }, + { label: 'Prompt features', slug: 'guides/prompt-features' }, + { label: 'Choose a coding agent', slug: 'guides/choosing-a-coding-agent' }, + { label: 'Software stacks', slug: 'guides/software-stacks' }, + ], + }, + { + label: 'Connect tools & skills', + collapsed: true, + items: [ + { label: 'Import MCP servers', slug: 'guides/importing-mcp-servers' }, + { label: 'Import skills', slug: 'guides/importing-skills' }, + { label: 'Import VS Code extensions', slug: 'guides/importing-extensions' }, + { label: 'Use tools, skills & extensions', slug: 'guides/mcp-skills-extensions' }, + ], + }, ], }, { label: 'Reference', + collapsed: true, items: [ { label: 'REST API overview', slug: 'reference/rest-api' }, { label: 'Profile schema', slug: 'reference/profile-schema' }, @@ -130,6 +152,7 @@ export default defineConfig({ ...openAPISidebarGroups, { label: 'Resources', + collapsed: true, items: [ { label: 'FAQ', slug: 'resources/faq' }, { label: 'Data collection and privacy', slug: 'resources/data-collection' }, diff --git a/website/public/scope-showreel-poster.jpg b/website/public/scope-showreel-poster.jpg new file mode 100644 index 000000000..d687978af Binary files /dev/null and b/website/public/scope-showreel-poster.jpg differ diff --git a/website/public/scope-showreel.mp4 b/website/public/scope-showreel.mp4 new file mode 100644 index 000000000..0ccd31a31 Binary files /dev/null and b/website/public/scope-showreel.mp4 differ diff --git a/website/src/components/FlowDemo.astro b/website/src/components/FlowDemo.astro new file mode 100644 index 000000000..9449dacdd --- /dev/null +++ b/website/src/components/FlowDemo.astro @@ -0,0 +1,118 @@ +--- +import { sampleAgents, sampleProfiles } from '../scripts/sample-agents'; +import { demoCriteria, demoGates, exampleScenario, passedGateCount } from '../scripts/flow-demo-data'; + +const base = import.meta.env.BASE_URL.replace(/\/?$/, '/'); +const stages = ['Define a scenario', 'Compare profiles', 'Judge gates and criteria', 'Explore the results']; +const rowHeight = 116; +const rowGap = 12; +const graphHeight = sampleProfiles.length * rowHeight + (sampleProfiles.length - 1) * rowGap; +const center = graphHeight / 2; +const incoming = sampleProfiles.map((_, index) => { + const y = index * (rowHeight + rowGap) + rowHeight / 2; + const direction = Math.sign(y - center); + return direction === 0 ? `M210 ${center} H270` + : `M210 ${center} H228 Q240 ${center} 240 ${center + direction * 12} V${y - direction * 12} Q240 ${y} 252 ${y} H270`; +}).join(' '); +const outgoing = sampleProfiles.map((_, index) => { + const y = index * (rowHeight + rowGap) + rowHeight / 2; + const direction = Math.sign(center - y); + return direction === 0 ? `M490 ${center} H550` + : `M490 ${y} H508 Q520 ${y} 520 ${y + direction * 12} V${center - direction * 12} Q520 ${center} 532 ${center} H550`; +}).join(' '); +const wires = [incoming, outgoing, `M770 ${center} H830`]; +--- + + +
+
+
Interactive exampleMock data
+

No agents run here. All results are pre-written examples.

+
+
+ +
+
MOCK SCENARIO / {exampleScenario.id}1 base + {sampleProfiles.length - 1} variations / same task and criteria
+
+ +
+ +
+ {sampleProfiles.map((profile) => ( + + ))} +
+ + +
+
+
+
+
+
+ {stages.map((stage, index) => )} +
+
+
+
Step 01 / Define

Give every agent the same starting line.

{exampleScenario.context}

+
+ TASK PROMPT / EXAMPLE +

{exampleScenario.prompt}

+
+ +
+

Illustrative demo only. No agents are running. All outcomes, token counts, and durations are invented, not measured benchmarks or agent rankings. Planned agents are not available today and are not included in the sample runs. Select any step to explore.

+

+ +
+ + diff --git a/website/src/components/Header.astro b/website/src/components/Header.astro new file mode 100644 index 000000000..aca1ce9c6 --- /dev/null +++ b/website/src/components/Header.astro @@ -0,0 +1,32 @@ +--- +import DefaultHeader from '@astrojs/starlight/components/Header.astro'; +import Search from '@astrojs/starlight/components/Search.astro'; +import ThemeSelect from '@astrojs/starlight/components/ThemeSelect.astro'; +import SiteTitle from './SiteTitle.astro'; + +const isHome = Astro.locals.starlightRoute.entry.id === ''; +const base = import.meta.env.BASE_URL.replace(/\/?$/, '/'); +--- + +{isHome ? ( +
+ + +
+
+) : } + + diff --git a/website/src/components/Home.astro b/website/src/components/Home.astro new file mode 100644 index 000000000..c99371ce7 --- /dev/null +++ b/website/src/components/Home.astro @@ -0,0 +1,151 @@ +--- +import CommunityTeaser from './community/CommunityTeaser.astro'; +import FlowDemo from './FlowDemo.astro'; + +const base = import.meta.env.BASE_URL.replace(/\/?$/, '/'); +const paths = [ + { + number: '01', + title: 'Start in the Portal', + description: 'Get access, submit your first task, and follow the results in your browser.', + href: 'getting-started/first-run/', + label: 'Your first run', + tag: 'New to Scope', + }, + { + number: '02', + title: 'Work from your terminal', + description: 'Install the CLI and make benchmarks part of your development workflow.', + href: 'getting-started/install-cli/', + label: 'Set up the CLI', + tag: 'For developers', + }, + { + number: '03', + title: 'Build it into your tools', + description: 'Explore request examples and the searchable, generated API reference.', + href: 'reference/rest-api/', + label: 'Explore the API', + tag: 'For integrations', + }, +]; +--- + +
+
+
The agentic experience, measured
+

Your product.
Their agents. The whole picture.

+

+ How do coding agents really handle your product? Give them the same + task, define what good looks like, and see the evidence. That's Scope. +

+ +

Your tasks. Your criteria. Repeatable experiments.

+ +
+ +
+ Silent Scope showreel: a task prompt, a criteria graph, ordered gates, + repeated runs, an agent trajectory, and a before-and-after comparison. +
+
+ + +
+
+
+
+ Built to evaluate + GitHub Copilot + + Claude Code + Meet the agents +
+
+ +
+
+

From a prompt to a picture

One task. Follow every step.

+
+ +
+ +
+
+

Beyond a pass or fail

Find the why behind the result.

+

A score is a starting point. Understand the task, the setup, and the path an agent took.

+
+ +
+ +
+
+

Make your next move

Less searching. More building.

+ Learn the core concepts +
+ + +
+ + + +
+

Bring your own question

+

What will your next
benchmark reveal?

+

Start with one task you care about. Scope helps you see the rest.

+ Get started + You'll need access to a Scope deployment. No account is needed for this demo. +
+ +
+ + diff --git a/website/src/components/PageTitle.astro b/website/src/components/PageTitle.astro new file mode 100644 index 000000000..91e2e2eb8 --- /dev/null +++ b/website/src/components/PageTitle.astro @@ -0,0 +1,7 @@ +--- +import DefaultPageTitle from '@astrojs/starlight/components/PageTitle.astro'; +--- + +{Astro.locals.starlightRoute.entry.id === '' + ? + : } diff --git a/website/src/components/SiteTitle.astro b/website/src/components/SiteTitle.astro index f49dc076e..b6482220e 100644 --- a/website/src/components/SiteTitle.astro +++ b/website/src/components/SiteTitle.astro @@ -14,9 +14,6 @@ const { siteTitle, siteTitleHref } = Astro.locals.starlightRoute; role="img" aria-label={config.logo?.alt} > - {/* Single ECG pulse. It stays put; its amplitude - oscillates low <-> high around the baseline, so the - wave "beats" without moving sideways. */} diff --git a/website/src/components/community/CommunityTeaser.astro b/website/src/components/community/CommunityTeaser.astro index 7a7624e03..92db04a6c 100644 --- a/website/src/components/community/CommunityTeaser.astro +++ b/website/src/components/community/CommunityTeaser.astro @@ -11,10 +11,9 @@ const featured = talks.find((t) => t.data.youtubeId) ?? talks[0]; const allHref = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/community/articles-and-talks/`; --- -
-
- From the community -

Articles & talks

+
+
+

From the community

Articles & talks

What the community has learned evaluating agentic product experiences with Scope, from the blog and the stage.

@@ -33,5 +32,5 @@ const allHref = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/community/articl }
- See all articles & talks → + See all articles & talks
diff --git a/website/src/content/docs/getting-started/install-cli.md b/website/src/content/docs/getting-started/install-cli.md index 1cb38a3a2..5c7f164a2 100644 --- a/website/src/content/docs/getting-started/install-cli.md +++ b/website/src/content/docs/getting-started/install-cli.md @@ -26,9 +26,8 @@ This downloads and installs the latest release to `SCOPE_INSTALL_DIR` environment variable: ```bash -SCOPE_INSTALL_DIR=~/bin \ - gh api repos/growth-ecosystems/scope-doc/contents/install-cli.sh \ - -H "Accept: application/vnd.github.raw" | bash +gh api repos/growth-ecosystems/scope-doc/contents/install-cli.sh \ + -H "Accept: application/vnd.github.raw" | SCOPE_INSTALL_DIR=~/bin bash ``` ## Add to PATH diff --git a/website/src/content/docs/guides/defining-criteria.mdx b/website/src/content/docs/guides/defining-criteria.mdx index f784266fc..2a105581d 100644 --- a/website/src/content/docs/guides/defining-criteria.mdx +++ b/website/src/content/docs/guides/defining-criteria.mdx @@ -12,6 +12,25 @@ stable ID and a `prompt` describing what to look for, and a [request](/guides/submitting-requests-portal/) references criteria by ID. +## Gates and reusable criteria + +**Gates** are the stages of a run. **Criteria** are the reusable checks +that define success within each stage. For example, a run can use the +**Requirements**, **Build**, and **Test** gates, in that order. + +Select criteria from your library for each gate. A criterion can declare +which gates it is compatible with using `gates`; the criteria within a +gate still form a dependency graph. Gates are not themselves criteria. + +Each gate has its own iteration budget. If it does not pass before +exhausting that budget, the pipeline stops and later gates are recorded +as skipped. Separately, a failed criterion causes its dependent criteria +to be skipped inside the gate. + +The [interactive example](/#see-it-work) illustrates both levels: the +ordered gates and an expandable criteria graph. Its results, token +counts, and durations are fictional examples, not benchmark measurements. + ## The criteria graph Every criterion lives in a **directed acyclic graph (DAG)**. diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 2a1134b64..3aa222695 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -1,219 +1,10 @@ --- title: Scope -description: The benchmark for agentic coding. Measure how coding agents behave on your tasks — across agents, at scale. +description: See how coding agents handle your product. Run repeatable tasks, evaluate against your criteria, and understand what happened. template: splash tableOfContents: false --- -import { Card, CardGrid } from '@astrojs/starlight/components'; -import CommunityTeaser from '../../components/community/CommunityTeaser.astro'; +import Home from '../../components/Home.astro'; -
- -{/* ============================ HERO ============================ */} -
-
-

Evaluate the agentic experience of your products

- -

- Run one scenario across GitHub Copilot, Claude Code, and VS Code. - Score every run against criteria you define, and see how your - agentic experience holds up across all of them — at scale. -

- - -
- - {/* Static "scenario coverage" proof panel */} - -
- -{/* ============================ HOW IT WORKS ============================ */} -
-
- How it works -

One task, every agent, measured the same way

-

Scope submits your task to each agent, scores every run against the criteria you define, and lines the results up side by side.

-
- -
-
- 01 -

Submit a task

-

Hand one coding task to GitHub Copilot, Claude Code, or VS Code with the Copilot driver extension — with the profile you want.

-
-
- 02 -

Score against criteria

-

Each run is evaluated against your criteria DAG. Gate child criteria on their parents, or leave them as independent roots.

-
-
- 03 -

Compare trajectories

-

See not just whether it worked, but how each agent got there — the tools it reached for and where it got stuck.

-
-
-
- -{/* ============================ FEATURES ============================ */} -
-
- What you can do -

Not “did it work?” — how did it get there?

-

Scope drives existing agents against tasks you control, then captures what they asked for, what tools they reached for, and where they got stuck.

-
- -
-
- 01 -

Submit a coding task

-

Hand a task to GitHub Copilot, Claude Code, or VS Code with the Copilot driver extension.

-
-
- 02 -

Save reusable profiles

-

Bundle worker, model, version, MCP servers, skills, and extensions to re-run a setup consistently.

-
-
- 03 -

Define criteria as a DAG

-

Describe what “good” means. Gate child criteria on their parents, or leave them as independent roots.

-
-
- 04 -

Watch runs live

-

Stream logs straight from the worker as each run executes, attempt by attempt.

-
-
- 05 -

Slice with prompt features

-

Scope detects characteristics on your prompts so you can compare across heterogeneous tasks.

-
-
- 06 -

Automate everything

-

Submit runs, manage profiles and criteria, and fetch results through the REST API or the CLI.

-
-
-
- -{/* ============================ CHANNELS ============================ */} -
-
- Three ways to drive it -

Portal, REST API, or CLI

-
- -
-
- Point & click -

Portal

-

Submit requests, configure profiles and criteria, and watch runs stream — all from the browser.

- Submit from the Portal → -
-
- Integrate -

REST API

-

Wire Scope into your pipelines and tooling with a single authenticated request.

- Read the API guide → -
-
- Terminal -

CLI

-

Run benchmarks from the terminal with real-time log streaming.

- Install the CLI → -
-
-
- -{/* ============================ DOCS ============================ */} -
-
- Documentation -

Explore the docs

-
- - - - Understand what Scope measures and when to reach for it. [Read the introduction](/introduction/what-is-scope/). - - - Run a task end to end from the Portal in a few minutes. [Get started](/getting-started/first-run/). - - - Automate request submission and integrate Scope into your tools. [API guide](/guides/submitting-requests-api/). - - - Endpoint reference, configuration schemas, and worker capabilities. [Browse the reference](/reference/rest-api/). - - -
- -{/* ============================ COMMUNITY ============================ */} - - -{/* ============================ CTA ============================ */} -
-

Stop guessing which agent is better. Measure it.

-

Spin up your first benchmark in minutes — from the Portal, the API, or the CLI.

- -
- -
+ diff --git a/website/src/content/docs/resources/glossary.md b/website/src/content/docs/resources/glossary.md index 4b6b6a3e1..e1cfefad0 100644 --- a/website/src/content/docs/resources/glossary.md +++ b/website/src/content/docs/resources/glossary.md @@ -6,7 +6,7 @@ description: Definitions of the core Scope concepts. ## Request What you submit to Scope. A request bundles a task prompt, a -criteria set, and a profile (or inline runtime configuration). MS +criteria graph, and a profile (or inline runtime configuration). Scope creates one or more runs per request. See [Submitting requests (Portal)](/guides/submitting-requests-portal/). @@ -19,17 +19,18 @@ new run on the same request. Logs and reports are produced per run. ## Task prompt The text instructions sent to the agent. Stored in a shared catalog, -de-duplicated by text: when you submit a request with task text MS +de-duplicated by text: when you submit a request with task text Scope has seen before, the request links to the existing record -rather than creating a duplicate. The entity prompt-feature detection -runs against. See [Managing task prompts](/guides/managing-task-prompts/). +rather than creating a duplicate. Prompt-feature detection runs +against this text. See [Managing task prompts](/guides/managing-task-prompts/). ## Criteria / Criterion A criterion is a single, observable statement the judge evaluates the -run's output against. A **criteria set** is a named, reusable -collection of criteria, optionally arranged as a DAG. Referenced from -a request by ID. See [Defining evaluation criteria](/guides/defining-criteria/). +run's output against. Criteria form a **directed acyclic graph (DAG)**. +Each criterion can depend on other criteria; a criterion without +dependencies is a root of the graph. See +[Defining evaluation criteria](/guides/defining-criteria/). ## Judge @@ -56,8 +57,7 @@ code, skill commits, and extension versions. ## Worker -The runtime that drives an AI coding agent during a run. Scope -ships three workers — see +The runtime that drives an AI coding agent during a run. See [Coding agents & capabilities](/reference/workers/). ## ACP (Agent Client Protocol) @@ -79,8 +79,8 @@ commit hash so re-runs are reproducible. ## Extension -A VS Code extension installed for the duration of a run. Only the VS -VS Code Copilot coding agent supports extensions. +A VS Code extension installed for the duration of a run. See +[Coding agents & capabilities](/reference/workers/) for compatibility. ## Prompt feature diff --git a/website/src/scripts/flow-demo-data.test.ts b/website/src/scripts/flow-demo-data.test.ts new file mode 100644 index 000000000..9bc76c688 --- /dev/null +++ b/website/src/scripts/flow-demo-data.test.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { describe, expect, it } from 'vitest'; +import { sampleAgents, sampleProfiles } from './sample-agents'; +import { criterionOutcomes, demoCriteria, exampleScenario, formatDelta, formatDuration, gateOutcomes, passedGateCount, totalTokens } from './flow-demo-data'; + +describe('interactive example fixtures', () => { + it('only runs supported agents and identifies a single base profile', () => { + expect(sampleProfiles.filter((profile) => profile.id === 'base')).toHaveLength(1); + expect(sampleProfiles.every((profile) => profile.agent.status === 'Supported today')).toBe(true); + expect(sampleAgents.filter((agent) => agent.status === 'Planned').map((agent) => agent.name)).toEqual(['OpenAI Codex', 'OpenCode']); + expect(sampleProfiles[1].agent).toBe(sampleProfiles[0].agent); + }); + + it('provides one task-board example with complete metrics for every profile', () => { + expect(exampleScenario.id).toBe('task-board'); + expect(Object.keys(exampleScenario.results)).toEqual(sampleProfiles.map((profile) => profile.id)); + for (const profile of sampleProfiles) { + const result = exampleScenario.results[profile.id]; + expect(totalTokens(result)).toBe(result.inputTokens + result.outputTokens); + expect(result.inputTokens).toBeGreaterThan(0); + expect(result.outputTokens).toBeGreaterThan(0); + expect(result.durationSeconds).toBeGreaterThan(0); + expect(criterionOutcomes(result).size).toBe(demoCriteria.length); + } + }); + + it('skips later gates after a failed build', () => { + const result = exampleScenario.results.agent; + expect(gateOutcomes(result)).toEqual(['Pass', 'Fail', 'Skipped']); + expect([...criterionOutcomes(result).values()]).toEqual(['Pass', 'Fail', 'Skipped', 'Skipped', 'Skipped']); + }); + + it('skips both dependent checks when their parent fails', () => { + const result: typeof exampleScenario.results.base = { ...exampleScenario.results.base, failedCriterion: 'tests_run' }; + expect(gateOutcomes(result)).toEqual(['Pass', 'Pass', 'Fail']); + expect([...criterionOutcomes(result).values()]).toEqual(['Pass', 'Pass', 'Fail', 'Skipped', 'Skipped']); + }); + + it('does not skip a sibling criterion when the edge-case check fails', () => { + expect([...criterionOutcomes(exampleScenario.results.base).values()]).toEqual(['Pass', 'Pass', 'Pass', 'Pass', 'Fail']); + }); + + it('reports all gates passed only when every criterion passes', () => { + expect(gateOutcomes(exampleScenario.results.skills)).toEqual(['Pass', 'Pass', 'Pass']); + }); + + it('derives the chart from the profile outcomes', () => { + expect(sampleProfiles.map((profile) => passedGateCount(exampleScenario.results[profile.id]))).toEqual([2, 3, 1]); + }); + + it('formats exact, signed deltas against the base', () => { + expect(formatDelta(18000, 21600, 'tokens')).toBe('-3,600 tokens vs base'); + expect(formatDelta(23400, 13500, 'tokens')).toBe('+9,900 tokens vs base'); + expect(formatDelta(128, 154, 'sec')).toBe('-26 sec vs base'); + expect(formatDelta(154, 154, 'sec')).toBe('Same as base'); + expect(formatDuration(154)).toBe('2m 34s'); + expect(formatDuration(60)).toBe('1m 00s'); + }); +}); diff --git a/website/src/scripts/flow-demo-data.ts b/website/src/scripts/flow-demo-data.ts new file mode 100644 index 000000000..7ae01aab4 --- /dev/null +++ b/website/src/scripts/flow-demo-data.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { SampleProfile } from './sample-agents'; + +export const demoGates = [ + { id: 'select', label: 'Requirements' }, + { id: 'build', label: 'Build' }, + { id: 'test', label: 'Test' }, +] as const; + +export const demoCriteria = [ + { id: 'implements_task', gate: 'select', label: 'Task implemented', dependsOn: [] }, + { id: 'builds', gate: 'build', label: 'Build succeeds', dependsOn: [] }, + { id: 'tests_run', gate: 'test', label: 'Tests execute', dependsOn: [] }, + { id: 'tests_pass', gate: 'test', label: 'Tests pass', dependsOn: ['tests_run'] }, + { id: 'edge_case', gate: 'test', label: 'Handles empty input', dependsOn: ['tests_run'] }, +] as const; + +type CriterionId = typeof demoCriteria[number]['id']; +export type DemoOutcome = 'Pass' | 'Fail' | 'Skipped'; + +interface DemoResult { + failedCriterion: CriterionId | null; + inputTokens: number; + outputTokens: number; + durationSeconds: number; +} + +interface DemoScenario { + id: string; + title: string; + short: string; + prompt: string; + context: string; + results: Record; +} + +// Invented fixtures teach how to read a comparison, not which setup performs best. +export const exampleScenario: DemoScenario = { + id: 'task-board', + title: 'Build a task board', + short: 'Add, complete, and filter tasks.', + prompt: 'Build a task board. Users can add tasks, mark them complete, and filter by status. An empty title must not create a task.', + context: 'Create a task board with add, complete, and filter actions. Define the expected behavior before the agents start.', + results: { + base: { failedCriterion: 'edge_case', inputTokens: 18400, outputTokens: 3200, durationSeconds: 154 }, + skills: { failedCriterion: null, inputTokens: 15100, outputTokens: 2900, durationSeconds: 128 }, + agent: { failedCriterion: 'builds', inputTokens: 9900, outputTokens: 1900, durationSeconds: 82 }, + }, +}; + +export function criterionOutcomes(result: DemoResult): Map { + const outcomes = new Map(); + let priorGateFailed = false; + for (const gate of demoGates) { + let gateFailed = false; + for (const criterion of demoCriteria.filter((item) => item.gate === gate.id)) { + const skipped = priorGateFailed || criterion.dependsOn.some((id) => outcomes.get(id) !== 'Pass'); + const outcome = skipped ? 'Skipped' : criterion.id === result.failedCriterion ? 'Fail' : 'Pass'; + outcomes.set(criterion.id, outcome); + if (outcome === 'Fail') gateFailed = true; + } + priorGateFailed ||= gateFailed; + } + return outcomes; +} + +export function gateOutcomes(result: DemoResult): DemoOutcome[] { + const outcomes = criterionOutcomes(result); + return demoGates.map((gate) => { + const values = demoCriteria.filter((criterion) => criterion.gate === gate.id).map((criterion) => outcomes.get(criterion.id)); + return values.includes('Fail') ? 'Fail' : values.every((value) => value === 'Pass') ? 'Pass' : 'Skipped'; + }); +} + +export function totalTokens(result: DemoResult): number { + return result.inputTokens + result.outputTokens; +} + +export function passedGateCount(result: DemoResult): number { + return gateOutcomes(result).filter((outcome) => outcome === 'Pass').length; +} + +export function formatDuration(seconds: number): string { + return `${Math.floor(seconds / 60)}m ${String(seconds % 60).padStart(2, '0')}s`; +} + +export function formatDelta(value: number, base: number, unit: string): string { + const delta = value - base; + return delta === 0 ? 'Same as base' : `${delta > 0 ? '+' : ''}${delta.toLocaleString('en-US')} ${unit} vs base`; +} diff --git a/website/src/scripts/flow-demo.ts b/website/src/scripts/flow-demo.ts new file mode 100644 index 000000000..fcb4660e1 --- /dev/null +++ b/website/src/scripts/flow-demo.ts @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { sampleProfiles } from './sample-agents'; +import { criterionOutcomes, demoCriteria, demoGates, exampleScenario, formatDelta, formatDuration, gateOutcomes, passedGateCount, totalTokens } from './flow-demo-data'; + +const headings = [ + 'Give every agent the same starting line.', + 'Start with a base. Change the setup.', + 'Gates are stages. Criteria define success.', + 'Compare quality, tokens, and time.', +]; + +class ScopeFlow extends HTMLElement { + private stage = 0; + private timer: ReturnType | undefined; + private playing = false; + private observer: IntersectionObserver | undefined; + private events: AbortController | undefined; + private motion = window.matchMedia('(prefers-reduced-motion: reduce)'); + private visible = false; + private interacted = false; + + connectedCallback() { + this.events = new AbortController(); + const { signal } = this.events; + this.querySelectorAll('button').forEach((control) => { control.disabled = false; }); + this.addEventListener('click', (event) => { + if (!(event.target instanceof Element)) return; + if (event.target.closest('summary')) { + this.interacted = true; + this.stop(); + return; + } + const button = event.target.closest('button'); + if (!button) return; + this.interacted = true; + if (button.hasAttribute('data-play')) { + if (this.motion.matches) { + this.stop(); + this.stage = (this.stage + 1) % 4; + this.render(); + } else if (this.playing) { + this.stop(); + } else { + if (this.stage === 3) this.stage = 0; + this.playing = true; + this.render(); + this.schedule(); + } + } else if (button.hasAttribute('data-reset')) { + this.stop(); + this.stage = 0; + this.render(); + } else { + const step = button.dataset.step ?? button.dataset.node; + if (step !== undefined) { + this.stop(); + this.stage = Number(step); + this.render(); + } + } + }, { signal }); + this.motion.addEventListener('change', () => this.stop(), { signal }); + document.addEventListener('visibilitychange', () => { + if (document.hidden) this.stop(); + }, { signal }); + this.observer = new IntersectionObserver(([entry]) => { + this.visible = entry.isIntersecting; + if (!this.visible) { + this.stop(); + } else if (!this.interacted && !this.motion.matches && !document.hidden) { + this.interacted = true; + this.playing = true; + this.render(); + this.schedule(); + } + }, { threshold: 0.25 }); + this.observer.observe(this); + this.render(false); + } + + disconnectedCallback() { + clearTimeout(this.timer); + this.observer?.disconnect(); + this.events?.abort(); + } + + private schedule() { + clearTimeout(this.timer); + if (!this.playing || !this.visible || document.hidden) return; + this.timer = setTimeout(() => { + this.stage += 1; + if (this.stage === 3) this.playing = false; + this.render(); + if (this.playing) this.schedule(); + }, 2400); + } + + private stop() { + clearTimeout(this.timer); + this.playing = false; + // Pausing must not replace an open disclosure or its focused contents. + this.renderPlayback(); + } + + private renderPlayback() { + this.dataset.playing = String(this.playing); + this.text('[data-play]', this.motion.matches ? (this.stage === 3 ? 'Start again' : 'Next step') : this.playing ? 'Pause animation' : this.stage === 3 ? 'Replay animation' : this.stage === 0 ? 'Play animation' : 'Resume animation'); + } + + private text(selector: string, value: string) { + const elements = this.querySelectorAll(selector); + if (elements.length === 0) throw new Error(`Missing Scope demo element: ${selector}`); + elements.forEach((element) => { element.textContent = value; }); + } + + private render(announce = true) { + this.dataset.stage = String(this.stage); + this.renderPlayback(); + const explorer = this.querySelector('[data-criteria-explorer]'); + if (!explorer) throw new Error('Missing Scope demo criteria explorer'); + explorer.hidden = this.stage !== 2; + this.querySelectorAll('[data-step], [data-node]').forEach((element) => { + const index = Number(element.dataset.step ?? element.dataset.node); + if (index === this.stage) element.setAttribute('aria-current', 'step'); + else element.removeAttribute('aria-current'); + element.dataset.complete = String(index < this.stage); + }); + this.querySelectorAll('[data-wire]').forEach((element) => { + element.dataset.active = String(Number(element.dataset.wire) <= this.stage); + }); + this.text('[data-stage-label]', `Step 0${this.stage + 1} / ${['Define', 'Execute', 'Evaluate', 'Understand'][this.stage]}`); + this.text('[data-detail-title]', headings[this.stage]); + this.text('[data-detail-body]', [ + exampleScenario.context, + 'Keep the task and criteria fixed. Compare the base profile with an alternate that adds a skill, or one that changes the agent and model. Each profile gets its own run.', + 'The judge evaluates each gate using checks selected from your library of reusable criteria. This example uses Requirements, Build, and Test gates. If a gate fails after its iteration budget, later gates are skipped.', + 'Read each variation against the base profile, including input/output tokens and run duration. A faster failed run is not a better result. These numbers and outcomes are fictional, not measured comparisons.', + ][this.stage]); + const panel = this.querySelector('[data-detail-panel]'); + if (!panel) throw new Error('Missing Scope demo detail panel'); + const evidenceOpen = panel.querySelector('.flow-result-evidence')?.open ?? false; + panel.replaceChildren(); + if (this.stage === 0) { + this.appendText(panel, 'span', 'TASK PROMPT / EXAMPLE', 'flow-code-label'); + this.appendText(panel, 'p', exampleScenario.prompt); + } else if (this.stage === 1) { + this.appendText(panel, 'span', 'SAME TASK / BASE + ALTERNATE PROFILES', 'flow-code-label'); + for (const profile of sampleProfiles) { + const item = document.createElement('div'); + item.className = 'flow-profile-setup'; + this.appendText(item, 'span', profile.label, 'flow-profile-label').dataset.base = String(profile.id === 'base'); + this.appendText(item, 'strong', `${profile.agent.name} / ${profile.name}`); + this.appendText(item, 'p', profile.setup); + panel.append(item); + } + } else if (this.stage === 2) { + this.appendText(panel, 'span', 'REUSABLE CRITERIA LIBRARY / EXAMPLE SELECTION', 'flow-code-label'); + for (const gate of demoGates) { + const group = document.createElement('div'); + group.className = 'flow-library-group'; + this.appendText(group, 'strong', `${gate.label} gate`); + for (const criterion of demoCriteria.filter((item) => item.gate === gate.id)) { + this.appendText(group, 'span', criterion.label, 'flow-library-criterion'); + } + panel.append(group); + } + } else { + this.renderResults(panel); + const evidence = panel.querySelector('.flow-result-evidence'); + if (evidence) evidence.open = evidenceOpen; + } + if (announce) this.text('[data-announcement]', `${exampleScenario.title}. Step ${this.stage + 1} of 4. ${headings[this.stage]}${this.stage === 3 ? ' Example complete. Results are fictional.' : ''}`); + } + + private renderResults(panel: Element) { + const body = this.createTable(panel, ['Profile', 'Gates passed', 'Tokens (input + output)', 'Run duration'], `${exampleScenario.title} / mock outcomes and metrics`); + for (const profile of sampleProfiles) { + const result = exampleScenario.results[profile.id]; + const base = exampleScenario.results.base; + const row = body.insertRow(); + row.dataset.profile = profile.id; + this.profileHeader(row, profile); + const gates = gateOutcomes(result); + const gateCell = row.insertCell(); + this.appendText(gateCell, 'strong', `${passedGateCount(result)} / ${demoGates.length}`); + for (const [index, gate] of demoGates.entries()) { + const state = document.createElement('span'); + state.className = 'flow-result-detail'; + state.dataset.result = gates[index].toLowerCase(); + state.textContent = `${gate.label}: ${gates[index]}`; + gateCell.append(state); + } + const tokens = row.insertCell(); + this.appendText(tokens, 'strong', totalTokens(result).toLocaleString('en-US')); + this.appendText(tokens, 'span', `${result.inputTokens.toLocaleString('en-US')} in / ${result.outputTokens.toLocaleString('en-US')} out`, 'flow-result-detail'); + this.appendText(tokens, 'span', profile.id === 'base' ? 'Reference' : formatDelta(totalTokens(result), totalTokens(base), 'tokens'), 'flow-metric-delta'); + const duration = row.insertCell(); + this.appendText(duration, 'strong', formatDuration(result.durationSeconds)); + this.appendText(duration, 'span', profile.id === 'base' ? 'Reference' : formatDelta(result.durationSeconds, base.durationSeconds, 'sec'), 'flow-metric-delta'); + } + this.appendText(panel, 'p', 'Tokens = input + output. Duration = elapsed run time, not this animation. Deltas compare each sample run with the base; less is not automatically better.', 'flow-metric-note'); + const details = document.createElement('details'); + details.className = 'flow-result-evidence'; + const summary = document.createElement('summary'); + summary.textContent = 'Inspect per-criterion outcomes'; + details.append(summary); + const outcomes = this.createTable(details, ['Profile', ...demoCriteria.map((criterion) => criterion.label)], `${exampleScenario.title} / mock criterion outcomes; skipped checks were not evaluated`); + for (const profile of sampleProfiles) { + const row = outcomes.insertRow(); + this.profileHeader(row, profile); + for (const outcome of criterionOutcomes(exampleScenario.results[profile.id]).values()) { + const cell = row.insertCell(); + cell.textContent = outcome; + cell.dataset.result = outcome.toLowerCase(); + } + } + panel.append(details); + } + + private createTable(parent: Element, headings: string[], captionText: string): HTMLTableSectionElement { + this.appendText(parent, 'p', 'Scroll horizontally to see all columns.', 'flow-table-hint'); + const container = document.createElement('div'); + container.className = 'flow-table-scroll'; + container.tabIndex = 0; + container.setAttribute('role', 'region'); + container.setAttribute('aria-label', captionText); + const table = document.createElement('table'); + table.className = 'flow-results'; + table.createCaption().textContent = captionText; + const row = table.createTHead().insertRow(); + for (const label of headings) { + const cell = document.createElement('th'); + cell.scope = 'col'; + cell.textContent = label; + row.append(cell); + } + container.append(table); + parent.append(container); + return table.createTBody(); + } + + private profileHeader(row: HTMLTableRowElement, profile: typeof sampleProfiles[number]) { + const header = document.createElement('th'); + header.scope = 'row'; + this.appendText(header, 'span', profile.label, 'flow-profile-label').dataset.base = String(profile.id === 'base'); + this.appendText(header, 'strong', profile.name); + this.appendText(header, 'span', profile.agent.name, 'flow-result-detail'); + row.append(header); + } + + private appendText(parent: Element, tag: 'span' | 'p' | 'strong', text: string, className?: string) { + const element = document.createElement(tag); + element.textContent = text; + if (className) element.className = className; + parent.append(element); + return element; + } +} + +if (!customElements.get('scope-flow')) customElements.define('scope-flow', ScopeFlow); diff --git a/website/src/scripts/sample-agents.ts b/website/src/scripts/sample-agents.ts new file mode 100644 index 000000000..281075403 --- /dev/null +++ b/website/src/scripts/sample-agents.ts @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Availability is separate from the fictional profiles and their results. +export const sampleAgents = [ + { name: 'GitHub Copilot', initial: 'G', status: 'Supported today' }, + { name: 'Claude Code', initial: 'C', status: 'Supported today' }, + { name: 'OpenAI Codex', initial: 'Co', status: 'Planned' }, + { name: 'OpenCode', initial: 'O', status: 'Planned' }, + { name: 'Cursor', initial: 'Cu', status: 'Not integrated' }, +] as const; + +export const sampleProfiles = [ + { id: 'base', agent: sampleAgents[0], label: 'Base', name: 'Baseline', setup: 'Pinned model, no extra skills.' }, + { id: 'skills', agent: sampleAgents[0], label: 'Var 1', name: 'Add a skill', setup: 'Same agent and model, with a task skill.' }, + { id: 'agent', agent: sampleAgents[1], label: 'Var 2', name: 'Change agent', setup: 'Different agent and model, same task.' }, +] as const; + +export type SampleProfile = typeof sampleProfiles[number]['id']; diff --git a/website/src/scripts/showreel.ts b/website/src/scripts/showreel.ts new file mode 100644 index 000000000..54ad3634a --- /dev/null +++ b/website/src/scripts/showreel.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +class ScopeShowreel extends HTMLElement { + private events: AbortController | undefined; + private observer: IntersectionObserver | undefined; + + connectedCallback() { + const video = this.querySelector('video'); + const button = this.querySelector('button'); + const status = this.querySelector('[role="status"]'); + const source = video?.dataset.src; + if (!video || !button || !status || !source) throw new Error('Missing Scope showreel elements'); + + this.events = new AbortController(); + const { signal } = this.events; + const motion = window.matchMedia('(prefers-reduced-motion: reduce)'); + let wantsPlayback = !motion.matches; + let visible = false; + let playbackRequest = 0; + + video.muted = true; + button.hidden = false; + button.disabled = false; + + const renderPlayback = () => { + button.textContent = video.paused ? 'Play showreel' : 'Pause showreel'; + }; + const reportFailure = (error: unknown) => { + wantsPlayback = false; + video.pause(); + status.textContent = 'The showreel is unavailable.'; + status.hidden = false; + button.disabled = true; + console.error('Scope showreel failed', error); + }; + const syncPlayback = () => { + const request = ++playbackRequest; + video.autoplay = wantsPlayback && visible && !document.hidden; + if (!video.autoplay) { + video.pause(); + return; + } + if (!video.getAttribute('src')) video.src = source; + if (!video.paused) return; + void video.play().catch((error: unknown) => { + // Pausing or a newer play request can invalidate a pending promise. + if (request !== playbackRequest || signal.aborted) return; + if (error instanceof DOMException && error.name === 'NotAllowedError') { + wantsPlayback = false; + renderPlayback(); + status.textContent = 'Autoplay is blocked. Select Play showreel to start.'; + status.hidden = false; + return; + } + reportFailure(error); + }); + }; + + button.addEventListener('click', () => { + wantsPlayback = video.paused; + syncPlayback(); + }, { signal }); + video.addEventListener('play', renderPlayback, { signal }); + video.addEventListener('pause', renderPlayback, { signal }); + video.addEventListener('playing', () => { status.hidden = true; }, { signal }); + video.addEventListener('error', () => reportFailure(video.error), { signal }); + motion.addEventListener('change', () => { + if (motion.matches) wantsPlayback = false; + syncPlayback(); + }, { signal }); + document.addEventListener('visibilitychange', syncPlayback, { signal }); + this.observer = new IntersectionObserver(([entry]) => { + visible = entry.isIntersecting; + syncPlayback(); + }, { threshold: 0.25 }); + this.observer.observe(this); + renderPlayback(); + } + + disconnectedCallback() { + this.events?.abort(); + this.observer?.disconnect(); + const video = this.querySelector('video'); + if (video) { + video.autoplay = false; + video.pause(); + } + } +} + +if (!customElements.get('scope-showreel')) customElements.define('scope-showreel', ScopeShowreel); diff --git a/website/src/styles/landing.css b/website/src/styles/landing.css index ded1469c4..3deb532be 100644 --- a/website/src/styles/landing.css +++ b/website/src/styles/landing.css @@ -3,574 +3,297 @@ * Licensed under the MIT License. */ -/* - * Scope — documentation landing (splash) styles. - * - * A professional, theme-aware design system for a developer-tool - * benchmark. Everything is driven by a small set of semantic tokens - * (surface / border / text / brand) that flip between light and dark, - * so the whole page inverts from one place. Neutral slate ramp, a - * single indigo brand accent, and green reserved for the "pass / score" - * semantic. No decorative motion — just type, space, and hairlines. - * - * Scoped via `:has(.scope-home)` so interior doc pages are untouched. - */ - -/* ================================================================ * - * 1. Design tokens - * ================================================================ */ - -html:has(.scope-home) { - /* Spacing scale (8px base) */ - --sp-1: 0.25rem; - --sp-2: 0.5rem; - --sp-3: 0.75rem; - --sp-4: 1rem; - --sp-6: 1.5rem; - --sp-8: 2rem; - --sp-12: 3rem; - --sp-16: 4rem; - - /* Radii */ - --r-sm: 0.5rem; - --r-md: 0.75rem; - --r-lg: 1rem; - --r-xl: 1.25rem; - - /* Type family: fast, high-quality system stack tuned like Inter */ - --font-display: 'Inter', ui-sans-serif, system-ui, -apple-system, - 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; - - background-color: var(--bg); -} - -/* Dark (default + explicit dark toggle) */ -html:has(.scope-home):not([data-theme='light']) { - color-scheme: dark; - - --bg: #0b0e16; - --surface: #10141f; - --surface-2: #151b28; - --border: rgba(255, 255, 255, 0.09); - --border-strong: rgba(255, 255, 255, 0.16); - --text: #e7eaf1; - --text-muted: rgba(231, 234, 241, 0.62); - --text-faint: rgba(231, 234, 241, 0.42); - - --brand: #8b95ff; /* indigo-400-ish, reads on dark */ - --brand-strong: #a5adff; - --ok: #4ade80; /* pass / score green */ - --warn: #fbbf24; - --fail: #f87171; - --on-brand: #0b0e16; - - /* Starlight dark chrome tokens */ - --sl-color-white: hsl(0, 0%, 100%); - --sl-color-gray-1: hsl(224, 20%, 94%); - --sl-color-gray-2: hsl(224, 8%, 79%); - --sl-color-gray-3: hsl(224, 7%, 58%); - --sl-color-gray-4: hsl(224, 8%, 38%); - --sl-color-gray-5: hsl(224, 12%, 24%); - --sl-color-gray-6: hsl(224, 15%, 16%); - --sl-color-gray-7: hsl(224, 18%, 12%); - --sl-color-black: hsl(224, 20%, 8%); - - --sl-color-accent-low: hsl(234, 54%, 22%); - --sl-color-accent: hsl(234, 90%, 66%); - --sl-color-accent-high: hsl(234, 100%, 86%); - - --sl-color-text: var(--sl-color-gray-2); - --sl-color-text-accent: var(--sl-color-accent-high); - --sl-color-bg: var(--bg); - --sl-color-bg-nav: transparent; - --sl-color-bg-sidebar: var(--sl-color-gray-6); - --sl-color-bg-inline-code: var(--sl-color-gray-5); - --sl-color-hairline-light: var(--sl-color-gray-5); - --sl-color-hairline-shade: var(--bg); -} - -/* Light toggle */ -html[data-theme='light']:has(.scope-home) { - color-scheme: light; - - --bg: #ffffff; - --surface: #ffffff; - --surface-2: #f8fafc; - --border: rgba(15, 23, 42, 0.1); - --border-strong: rgba(15, 23, 42, 0.18); - --text: #0f172a; - --text-muted: rgba(15, 23, 42, 0.62); - --text-faint: rgba(15, 23, 42, 0.42); - - --brand: #4f46e5; /* indigo-600 */ - --brand-strong: #4338ca; - --ok: #16a34a; - --warn: #d97706; - --fail: #dc2626; - --on-brand: #ffffff; - - --sl-color-bg: var(--bg); - --sl-color-bg-nav: transparent; -} - -/* ================================================================ * - * 2. Canvas + Starlight chrome - * ================================================================ */ - -body:has(.scope-home) { background: var(--bg) !important; } - -body:has(.scope-home) .page, -body:has(.scope-home) .main-frame, -body:has(.scope-home) .main-pane, -body:has(.scope-home) .content-panel { - background: transparent !important; -} - -/* hide Starlight's auto-generated page title on the landing route only */ -body:has(.scope-home) .content-panel:has(> .sl-container > h1#_top) { - display: none; -} - -body:has(.scope-home) header.header { - background: color-mix(in oklch, var(--bg) 82%, transparent); - backdrop-filter: saturate(140%) blur(12px); - border-bottom: 1px solid var(--border); -} - -.scope-home { - overflow-x: clip; - font-family: var(--font-display); - color: var(--text); -} -.scope-home > :first-child { margin-top: 0; } - -/* ================================================================ * - * 3. Shared primitives - * ================================================================ */ - -.scope-btn { - display: inline-flex; - align-items: center; - gap: var(--sp-2); - padding: 0.72rem 1.3rem; - border-radius: var(--r-sm); - font-size: 0.95rem; - font-weight: 600; - line-height: 1; - text-decoration: none; - border: 1px solid transparent; - transition: background 0.15s ease, border-color 0.15s ease, - color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; -} - -.scope-btn svg { width: 1.05rem; height: 1.05rem; } - -.scope-btn--primary { - color: var(--on-brand); - background: var(--brand); - box-shadow: 0 1px 2px rgba(15, 23, 42, 0.12); -} -.scope-btn--primary:hover { - background: var(--brand-strong); - transform: translateY(-1px); - box-shadow: 0 8px 24px -10px var(--brand); -} - -.scope-btn--ghost { - color: var(--text); - border-color: var(--border-strong); - background: var(--surface); -} -.scope-btn--ghost:hover { border-color: var(--brand); color: var(--brand); } - -/* ================================================================ * - * 4. HERO - * ================================================================ */ - -.scope-hero { - text-align: center; - padding: clamp(3rem, 8vw, 6rem) 0 0; -} - -.scope-hero__inner { max-width: 56rem; margin-inline: auto; } - -.scope-hero h1 { - margin: var(--sp-6) auto 0; - max-width: 18ch; - font-size: clamp(2.6rem, 6.5vw, 4.6rem); - line-height: 1.04; - letter-spacing: -0.032em; - font-weight: 700; - color: var(--text); -} - -.scope-hero h1 .grad { color: var(--brand); } - -.scope-hero .scope-subtitle { - margin: var(--sp-6) auto 0; - max-width: 46ch; - font-size: clamp(1.05rem, 2.1vw, 1.25rem); - line-height: 1.6; - color: var(--text-muted); -} - -.scope-actions { - display: flex; - flex-wrap: wrap; - gap: var(--sp-3); - justify-content: center; - margin-top: var(--sp-8); -} - -/* ---------------- Product proof panel ---------------- */ - -.scope-proof { - margin: clamp(3rem, 7vw, 4.5rem) auto 0; - max-width: 60rem; - border: 1px solid var(--border); - border-radius: var(--r-xl); - background: var(--surface); - box-shadow: 0 24px 60px -40px rgba(15, 23, 42, 0.55); - overflow: hidden; - text-align: left; -} - -.scope-proof__bar { - display: flex; - align-items: center; - gap: var(--sp-2); - padding: 0.7rem 1rem; - border-bottom: 1px solid var(--border); - background: var(--surface-2); -} - -.scope-proof__dots { display: flex; align-items: center; gap: 0.5rem; } -.scope-proof__dots i { - width: 0.72rem; height: 0.72rem; border-radius: 999px; - margin: 0; - background: var(--border-strong); - box-shadow: inset 0 0 0 0.5px rgba(0, 0, 0, 0.18); -} -/* macOS traffic lights: close / minimize / zoom */ -.scope-proof__dots i:nth-child(1) { background: #ff5f57; } -.scope-proof__dots i:nth-child(2) { background: #febc2e; } -.scope-proof__dots i:nth-child(3) { background: #28c840; } -.scope-proof__title { - margin-left: var(--sp-2); - font-family: var(--sl-font-mono, monospace); - font-size: 0.74rem; - color: var(--text-faint); -} -.scope-proof__run { - margin-left: auto; - font-family: var(--sl-font-mono, monospace); - font-size: 0.7rem; - color: var(--text-muted); -} - -.scope-board { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 0; -} - -.scope-lane { padding: 1.15rem 1.25rem 1.35rem; border-right: 1px solid var(--border); margin: 0; } -.scope-lane:last-child { border-right: none; } - -.scope-lane__top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: var(--sp-2); - min-height: 4.05rem; - margin-bottom: var(--sp-4); - padding-bottom: var(--sp-3); - border-bottom: 1px solid var(--border); -} - -.scope-lane__name { display: block; font-weight: 600; font-size: 0.95rem; line-height: 1.3; color: var(--text); } -.scope-lane__id { - display: block; - margin-top: 0.2rem; - font-family: var(--sl-font-mono, monospace); - font-size: 0.66rem; - line-height: 1.35; - color: var(--text-faint); +:root { + --sl-font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --sl-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --sl-color-accent-low: #202453; + --sl-color-accent: #7778ee; + --sl-color-accent-high: #c2c5ff; + --scope-bg: #0e111b; + --scope-surface: #161a27; + --scope-inset: #111521; + --scope-text: #f0f1f8; + --scope-muted: #a7afc5; + --scope-border: #303749; + --scope-brand: #b1b4ff; + --scope-brand-fill: #b1b4ff; + --scope-on-brand: #171a36; + --scope-pass: #83dcb0; + --scope-fail: #ffa498; + --scope-shadow: 0 24px 70px -36px #0009; +} +:root[data-theme='light'] { + --sl-color-accent-low: #eeedff; + --sl-color-accent: #514bc5; + --sl-color-accent-high: #36318a; + --scope-bg: #fafbfdfc; + --scope-surface: #ffffff; + --scope-inset: #f3f5fa; + --scope-text: #171c30; + --scope-muted: #576079; + --scope-border: #dce0eb; + --scope-brand: #5048bd; + --scope-brand-fill: #514ac6; + --scope-on-brand: #ffffff; + --scope-pass: #16754c; + --scope-fail: #af352c; + --scope-shadow: 0 24px 70px -36px #30376240; +} + +/* Keep the reading experience native to Starlight, with shared brand accents. */ +header.header { border-bottom-color: var(--scope-border); } +body:not(:has(.scope-home)) .sidebar-content a[aria-current='page'] { border-radius: 0.4rem; } +body:not(:has(.scope-home)) .sl-markdown-content { line-height: 1.8; } +body:not(:has(.scope-home)) .pagination-links a { border-radius: 0.75rem; } +:focus-visible { outline: 3px solid var(--scope-brand); outline-offset: 4px; } + +html:has(.scope-home) { --sl-content-width: 78rem; --sl-color-bg: var(--scope-bg); --sl-color-bg-nav: var(--scope-bg); } +body:has(.scope-home) { background: var(--scope-bg); } +body:has(.scope-home) .content-panel:has([data-home-title-placeholder]) { display: none; } +body:has(.scope-home) .content-panel { border: 0; padding-top: 0; } +body:has(.scope-home) .sl-container { max-width: 78rem; } +body:has(.scope-home) header.header { background: color-mix(in srgb, var(--scope-bg) 94%, transparent); backdrop-filter: blur(14px); } +.scope-home { color: var(--scope-text); font-family: var(--sl-font); line-height: 1.6; } +.scope-home * { box-sizing: border-box; } +.scope-home :is(h1,h2,h3,p) { margin: 0; } +.scope-home a { text-decoration: none; } +.scope-home button { font: inherit; } +.scope-home button { cursor: pointer; } +.scope-home button:disabled { cursor: default; opacity: 0.6; } +.scope-home :is(section, h2) { scroll-margin-top: 6rem; } +.scope-eyebrow { color: var(--scope-brand); font-size: 0.72rem; font-weight: 650; text-transform: uppercase; letter-spacing: 0.13em; } +.scope-signal { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--scope-pass); box-shadow: 0 0 0 4px color-mix(in srgb, var(--scope-pass) 12%, transparent); } +.scope-hero { position: relative; padding: clamp(3rem, 6vw, 4.5rem) 1rem 0; text-align: center; } +.scope-hero::before { content: ''; position: absolute; inset: 0 -1rem; pointer-events: none; z-index: -1; background: radial-gradient(ellipse at 50% 24%, color-mix(in srgb, var(--scope-brand) 9%, transparent), transparent 65%); } +.scope-hero > .scope-eyebrow { display: inline-flex; gap: 0.8rem; align-items: center; padding: 0.4rem 0.85rem; border: 1px solid var(--scope-border); background: var(--scope-surface); border-radius: 100px; font-size: 0.65rem; } +.scope-hero h1 { margin: 1.7rem auto 1.3rem; max-width: 19ch; font-size: clamp(2.8rem, 5.7vw, 5rem); font-weight: 650; line-height: 1.08; letter-spacing: -0.06em; text-wrap: balance; } +.scope-hero h1 span { color: var(--scope-brand); } +.scope-hero .scope-subtitle { max-width: 57ch; margin: auto; color: var(--scope-muted); font-size: clamp(1rem, 1.5vw, 1.15rem); line-height: 1.75; text-wrap: pretty; } +.scope-actions { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.75rem; margin-top: 1.8rem; } +.scope-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.7rem; min-height: 46px; padding: 0.65rem 1.2rem; border: 1px solid transparent; border-radius: 0.55rem; font-size: 0.875rem; font-weight: 600; line-height: 1.4; transition: background 180ms, box-shadow 180ms; white-space: nowrap; } +.scope-btn--primary { color: var(--scope-on-brand); background: var(--scope-brand-fill); box-shadow: 0 3px 7px #171a3612, inset 0 1px 0 #ffffff20; } +.scope-btn--primary:hover { background: color-mix(in srgb, var(--scope-brand-fill) 88%, var(--scope-text)); box-shadow: 0 4px 18px color-mix(in srgb, var(--scope-brand) 25%, transparent); } +.scope-btn--secondary { color: var(--scope-text); background: var(--scope-surface); border-color: var(--scope-border); } +.scope-btn--secondary:hover { background: var(--scope-inset); } +.scope-hero .scope-hero-note { margin-top: 1rem; color: var(--scope-muted); font-size: 0.75rem; } +/* The showreel is dark in both themes, so its overlay controls use fixed dark styling. */ +scope-showreel { display: block; max-width: 60rem; margin: 3rem auto 0; } +.scope-showreel-frame { position: relative; margin: 0; overflow: hidden; border: 1px solid var(--scope-border); border-radius: 1rem; background: #0b0d18; box-shadow: var(--scope-shadow), 0 0 0 6px color-mix(in srgb, var(--scope-brand) 7%, transparent); } +.scope-showreel-frame video { display: block; width: 100%; height: auto; aspect-ratio: 16 / 9; object-fit: cover; } +.scope-showreel-controls { position: absolute; right: 0.75rem; bottom: 0.75rem; left: 0.75rem; display: flex; flex-wrap: wrap; justify-content: end; align-items: center; gap: 0.5rem; pointer-events: none; text-align: right; } +.scope-showreel-controls > * { pointer-events: auto; } +.scope-showreel-controls button { min-height: 44px; padding: 0.5rem 0.85rem; border: 1px solid #ffffff33; border-radius: 0.5rem; background: #0b0d18c7; color: #e9eaf6; font-size: 0.75rem; backdrop-filter: blur(6px); } +.scope-showreel-controls button:hover { border-color: #ffffff80; } +.scope-showreel-controls button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; } +.scope-showreel-controls button:disabled { opacity: 0.6; } +.scope-showreel-controls [role='status'] { max-width: 36ch; padding: 0.45rem 0.65rem; border-radius: 0.4rem; background: #0b0d18d9; color: #e9eaf6; font-size: 0.75rem; } +.scope-agent-strip { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 1.4rem; margin-top: 3.7rem; padding: 1.5rem 0; border-top: 1px solid var(--scope-border); border-bottom: 1px solid var(--scope-border); font-size: 0.8rem; } +.scope-agent-strip > span:first-child { color: var(--scope-muted); font-size: 0.75rem; } +.scope-agent-strip strong { font-size: 1rem; font-weight: 600; } +.scope-agent-strip a { color: var(--scope-brand); } +.scope-strip-divider { width: 1px; height: 1.1rem; background: var(--scope-border); } +.scope-section { margin-top: clamp(3.5rem, 7vw, 6rem); } +.scope-demo-section { margin-top: 3.5rem; } +.scope-section-heading { display: flex; justify-content: space-between; align-items: end; gap: 2rem; margin-bottom: 1.8rem; } +.scope-section-heading h2 { margin-top: 0.55rem; font-size: clamp(1.8rem, 3.3vw, 2.65rem); font-weight: 600; letter-spacing: -0.04em; line-height: 1.2; text-wrap: balance; } +.scope-section-heading > p { max-width: 33ch; font-size: 0.9rem; color: var(--scope-muted); } +.scope-text-link { display: inline-flex; align-items: center; justify-content: space-between; gap: 0.75rem; color: var(--scope-brand); font-size: 0.85rem; font-weight: 600; } +.scope-text-link:hover { text-decoration: underline; } + +/* The interactive example uses a custom element, without a client-side UI framework. */ +.scope-flow { display: block; border: 1px solid var(--scope-border); border-radius: 1rem; background: var(--scope-surface); box-shadow: var(--scope-shadow); overflow: clip; } +.flow-toolbar { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 1rem 1.5rem; border-bottom: 1px solid var(--scope-border); } +.flow-demo-label { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: center; font-size: 0.85rem; } +.flow-demo-badge { padding: 0.1rem 0.5rem; color: var(--scope-muted); background: var(--scope-inset); border: 1px solid var(--scope-border); border-radius: 4px; font-family: var(--sl-font-mono); font-size: 0.65rem; } +.scope-home .flow-demo-note { margin-top: 0.4rem; color: var(--scope-muted); font-size: 0.75rem; } +.scope-home .flow-support { display: flex; flex-wrap: wrap; gap: 0.6rem 1.4rem; list-style: none; margin: 0; padding: 0.9rem 1.5rem; border-bottom: 1px solid var(--scope-border); } +.flow-support li { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; font-size: 0.75rem; } +.flow-support strong { font-weight: 500; } +.flow-support li > span { color: var(--scope-muted); border: 1px dashed var(--scope-border); border-radius: 4px; padding: 0.1rem 0.35rem; } +.flow-support [data-supported='true'] > span { color: var(--scope-pass); border-style: solid; background: color-mix(in srgb, var(--scope-pass) 7%, var(--scope-surface)); } +.flow-canvas { position: relative; padding: 1.2rem 2rem 1.75rem; background-color: var(--scope-inset); background-image: radial-gradient(var(--scope-border) 0.8px, transparent 0.8px); background-size: 18px 18px; } +.flow-canvas-label { display: flex; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem; font: 0.62rem var(--sl-font-mono); letter-spacing: 0.06em; color: var(--scope-muted); } +.flow-graph { position: relative; } +.flow-connections { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; } +.flow-wire, .flow-packets { fill: none; stroke: var(--scope-border); stroke-width: 2; } +.flow-packets { stroke: var(--scope-brand); stroke-dasharray: 8 16; } +.flow-packets path { opacity: 0; } +.flow-packets path[data-active='true'] { opacity: 0.75; } +[data-playing='true'] .flow-packets path[data-active='true'] { animation: flow-travel 1s linear infinite; } +@keyframes flow-travel { to { stroke-dashoffset: -48; } } +.flow-nodes { position: relative; display: grid; grid-template-columns: 21% 22% 22% 17%; justify-content: space-between; align-items: center; height: var(--flow-graph-height); } +.flow-node { display: flex; flex-direction: column; text-align: left; gap: 0.5rem; min-width: 0; padding: 1rem; border: 1px solid var(--scope-border); border-radius: 0.65rem; background: var(--scope-surface); color: var(--scope-text); box-shadow: 0 3px 8px #11182706; transition: border-color 200ms, box-shadow 200ms; } +.flow-node:hover { border-color: var(--scope-brand); } +.flow-node[aria-current='step'] { border-color: var(--scope-brand); box-shadow: 0 0 0 3px color-mix(in srgb, var(--scope-brand) 13%, transparent), 0 8px 24px #1118270c; } +.flow-node[data-complete='true'] { border-color: color-mix(in srgb, var(--scope-brand) 35%, var(--scope-border)); } +.flow-node strong { font-size: 0.9rem; line-height: 1.3; letter-spacing: -0.025em; } +.flow-node > span:not([class]), .flow-node-tag { font-size: 0.7rem; color: var(--scope-muted); line-height: 1.5; } +.flow-node-kicker { display: block; color: var(--scope-muted); font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.06em; } +.flow-node-icon { display: grid; place-items: center; width: 30px; height: 30px; color: var(--scope-brand); background: color-mix(in srgb, var(--scope-brand) 9%, var(--scope-surface)); border: 1px solid color-mix(in srgb, var(--scope-brand) 18%, var(--scope-border)); border-radius: 7px; margin-bottom: 0.3rem; } +.flow-node-icon svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; } +.flow-node-tag { border-top: 1px solid var(--scope-border); padding-top: 0.65rem; margin-top: 0.3rem; width: 100%; } +.flow-agent-nodes { display: grid; grid-template-rows: repeat(var(--flow-agent-count), minmax(0, 1fr)); gap: var(--flow-agent-gap); height: 100%; } +.flow-agent { position: relative; display: grid; grid-template-columns: 24px 1fr; column-gap: 0.6rem; row-gap: 0.2rem; align-items: center; padding: 0.65rem 0.8rem; width: 100%; } +.flow-agent strong { font-size: 0.75rem; } +.flow-agent .flow-node-kicker { font-size: 0.55rem; margin-top: 0.3rem; } +.flow-agent > span:nth-child(2) { min-width: 0; } +.flow-agent strong { display: block; } +.flow-profile-label { display: inline-block; padding: 0.05rem 0.4rem; border: 1px solid var(--scope-border); border-radius: 4px; color: var(--scope-brand); background: var(--scope-surface); font: 0.75rem/1.5 var(--sl-font-mono); white-space: nowrap; } +.flow-profile-label[data-base='true'] { background: var(--scope-brand-fill); color: var(--scope-on-brand); border-color: transparent; } +.flow-agent .flow-profile-label { margin-top: 0.4rem; } +.flow-profile-name { display: block; font-size: 0.75rem; color: var(--scope-muted); margin-top: 0.2rem; } +.flow-agent-initial { display: grid; place-items: center; width: 24px; height: 24px; border-radius: 6px; background: var(--scope-inset); border: 1px solid var(--scope-border); color: var(--scope-brand); font: 600 0.75rem var(--sl-font-mono); } +.flow-agent-status { grid-column: 1 / -1; border-top: 1px solid var(--scope-border); padding-top: 0.4rem; color: var(--scope-muted); font: 0.62rem var(--sl-font-mono); } +[data-stage='1'] .flow-agent-status { color: var(--scope-brand); } +.flow-criteria { display: flex; flex-direction: column; align-items: start; margin-top: 0.25rem; width: 100%; font: 0.6rem var(--sl-font-mono); color: var(--scope-muted); } +.flow-criteria > span { padding: 0.3rem 0.5rem; border: 1px solid var(--scope-border); border-radius: 4px; background: var(--scope-inset); } +.flow-criteria i { width: 1px; height: 9px; background: var(--scope-brand); margin-left: 0.8rem; } +.flow-report { padding: 0.9rem; } +.flow-chart { display: flex; align-items: end; gap: 5px; height: 44px; padding: 5px 0 0; width: 100%; border-bottom: 1px solid var(--scope-border); } +.flow-chart i { flex: 1; background: var(--scope-border); border-radius: 2px 2px 0 0; } +.flow-result-preview { color: var(--scope-muted); font: 0.75rem/1.5 var(--sl-font-mono); } +[data-stage='3'] .flow-chart i { background: var(--scope-brand); } +.flow-controls { display: flex; align-items: center; justify-content: space-between; gap: 1.25rem; padding: 1rem 1.5rem; border-block: 1px solid var(--scope-border); } +.flow-playback { display: flex; gap: 0.65rem; align-items: center; } +.flow-playback .scope-btn { min-width: 8.2rem; font-size: 0.75rem; min-height: 44px; } +.flow-reset { color: var(--scope-muted); border: 0; background: transparent; min-height: 44px; padding: 0.5rem; font-size: 0.75rem !important; } +.flow-reset:hover { color: var(--scope-brand); } +.flow-step-buttons { display: flex; gap: 0.5rem; } +.flow-step-buttons button { display: inline-flex; gap: 0.4rem; align-items: center; padding: 0.5rem 0.65rem; min-height: 44px; background: transparent; color: var(--scope-muted); border: 1px solid transparent; border-radius: 6px; font-size: 0.7rem; } +.flow-step-buttons button > span:first-child { font: 0.65rem var(--sl-font-mono); } +.flow-step-buttons button[aria-current='step'] { color: var(--scope-brand); border-color: var(--scope-border); background: var(--scope-inset); } +.flow-inspector { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2.5rem; padding: 1.6rem; min-height: 237px; align-items: start; } +.flow-inspector h3 { margin: 0.6rem 0; font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em; line-height: 1.4; } +.flow-inspector-copy > p { color: var(--scope-muted); font-size: 0.82rem; } +.flow-detail-panel { padding: 1rem 1.1rem; border: 1px solid var(--scope-border); border-radius: 0.5rem; background: var(--scope-inset); font: 0.73rem/1.7 var(--sl-font-mono); min-height: 166px; } +.flow-code-label { display: block; font: 0.6rem var(--sl-font-mono); color: var(--scope-muted); letter-spacing: 0.07em; margin-bottom: 0.8rem; } +.flow-detail-panel .flow-log-line { margin-top: 0.5rem; } +.flow-profile-setup + .flow-profile-setup, .flow-library-group + .flow-library-group { border-top: 1px solid var(--scope-border); padding-top: 0.75rem; margin-top: 0.75rem; } +.flow-profile-setup > strong { margin-left: 0.6rem; font: 600 0.8rem var(--sl-font); } +.flow-profile-setup > p { margin-top: 0.4rem; color: var(--scope-muted); font: 0.8rem/1.6 var(--sl-font); } +.flow-library-group { display: flex; flex-wrap: wrap; gap: 0.35rem; font-size: 0.75rem; } +.flow-library-group > strong { flex-basis: 100%; font-family: var(--sl-font); } +.flow-library-criterion { border: 1px solid var(--scope-border); padding: 0.2rem 0.45rem; border-radius: 4px; background: var(--scope-surface); } +.flow-criteria-explorer { grid-column: 1 / -1; border-top: 1px solid var(--scope-border); width: 100%; min-width: 0; } +.scope-flow summary { padding-block: 0.8rem; min-height: 44px; color: var(--scope-brand); cursor: pointer; font: 600 0.85rem/1.6 var(--sl-font); } +.flow-criteria-explorer > p { max-width: 85ch; font-size: 0.82rem; color: var(--scope-muted); } +.flow-gate-graph { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.5rem; margin: 1.25rem 0; } +.flow-gate-group { position: relative; padding: 1rem; border: 1px solid var(--scope-border); border-radius: 0.5rem; background: var(--scope-inset); } +.flow-gate-group:not(:last-child)::after { content: ''; position: absolute; top: 2rem; left: 100%; width: 1.5rem; border-top: 2px solid var(--scope-brand); } +.flow-gate-group h4 { font: 600 0.85rem var(--sl-font); margin: 0 0 0.75rem; } +.scope-home .flow-criterion-nodes { list-style: none; padding: 0; margin: 0; font: 0.75rem/1.6 var(--sl-font-mono); } +.flow-criterion-nodes li > span { display: inline-block; padding: 0.35rem 0.5rem; border: 1px solid var(--scope-border); border-radius: 4px; background: var(--scope-surface); } +.flow-criterion-nodes [data-dependent='true'] { position: relative; margin-left: 0.75rem; padding: 0.65rem 0 0 1rem; border-left: 2px solid var(--scope-brand); } +.flow-criterion-nodes [data-dependent='true']::before { content: ''; position: absolute; left: 0; top: 1.5rem; width: 1rem; border-top: 2px solid var(--scope-brand); } +[data-stage='3'] .flow-inspector { grid-template-columns: minmax(0, 1fr); gap: 1.25rem; } +[data-stage='3'] .flow-inspector-copy { max-width: 80ch; } +.flow-table-scroll { max-width: 100%; overflow-x: auto; } +.flow-table-hint { display: none; } +.flow-results { border-collapse: collapse; width: 100%; font-size: 0.7rem; } +.flow-results caption { text-align: left; color: var(--scope-muted); font-size: 0.6rem; padding-bottom: 0.5rem; } +.flow-results :is(th, td) { padding: 0.65rem 0.6rem; text-align: left; vertical-align: top; border-bottom: 1px solid var(--scope-border); font-weight: 400; } +.flow-results { min-width: 620px; font-variant-numeric: tabular-nums; } +.flow-results tbody th { min-width: 160px; } +.flow-results tbody th strong { display: block; margin-top: 0.35rem; } +.flow-results strong { font-weight: 600; } +.flow-result-detail, .flow-metric-delta { display: block; margin-top: 0.25rem; font-size: 0.75rem; color: var(--scope-muted); } +.flow-metric-delta { color: var(--scope-text); } +.scope-home .flow-metric-note { margin-top: 0.75rem; color: var(--scope-muted); font: 0.75rem/1.7 var(--sl-font); } +.flow-result-evidence { margin-top: 0.75rem; } +.flow-results thead th { color: var(--scope-muted); font-size: 0.6rem; } +.flow-result-profile { display: block; color: var(--scope-muted); font-size: 0.7rem; } +.flow-results [data-result='pass'] { color: var(--scope-pass); }.flow-results [data-result='fail'] { color: var(--scope-fail); }.flow-results [data-result='skipped'] { color: var(--scope-muted); } +.scope-home .flow-disclaimer { padding: 0 1.5rem 1rem; font-size: 0.68rem; color: var(--scope-muted); } +.scope-home :is(.flow-canvas-label, .flow-demo-badge, .flow-node-kicker, .flow-agent .flow-node-kicker, .flow-node-tag, .flow-criteria, .flow-agent-status, .flow-code-label, .flow-results, .flow-results caption, .flow-results thead th, .flow-step-buttons button, .flow-step-buttons button > span:first-child, .flow-disclaimer, .scope-path-top, .scope-profile-preview, .scope-trace-preview, .scope-mini-dag span, .scope-access-note) { font-size: 0.75rem; } +.flow-node > span:not([class]) { font-size: 0.75rem; } +.scope-flow:not(:defined) .flow-controls { display: none; } +.scope-flow:not(:defined) .flow-node { opacity: 1; } + +.scope-evidence-grid { display: grid; grid-template-columns: 1.2fr 1fr 1fr; gap: 1rem; } +.scope-evidence { display: flex; flex-direction: column; padding: 1.7rem; border: 1px solid var(--scope-border); border-radius: 0.8rem; background: var(--scope-surface); color: var(--scope-text); transition: border-color 180ms, box-shadow 180ms; } +.scope-evidence:hover, .scope-path:hover { border-color: var(--scope-brand); box-shadow: 0 5px 20px #11182708; } +.scope-evidence h3 { font-size: 1.25rem; font-weight: 600; letter-spacing: -0.03em; line-height: 1.35; margin: 0.7rem 0; } +.scope-evidence > p:not(.scope-eyebrow) { color: var(--scope-muted); font-size: 0.85rem; } +.scope-evidence > .scope-text-link { margin-top: auto; padding-top: 1.4rem; } +.scope-mini-dag { display: flex; align-items: center; justify-content: center; min-height: 130px; margin-bottom: 1.5rem; background: radial-gradient(ellipse, color-mix(in srgb, var(--scope-brand) 9%, transparent), transparent 75%); } +.scope-mini-dag span { border: 1px solid var(--scope-border); border-radius: 5px; padding: 0.45rem 0.6rem; font: 0.65rem var(--sl-font-mono); background: var(--scope-surface); } +.scope-mini-dag i { width: 18px; height: 1px; background: var(--scope-brand); } +.scope-profile-preview, .scope-trace-preview { display: grid; align-content: center; gap: 0.55rem; min-height: 130px; margin-bottom: 1.5rem; padding: 1rem; background: var(--scope-inset); border: 1px solid var(--scope-border); border-radius: 7px; font: 0.62rem var(--sl-font-mono); color: var(--scope-muted); } +.scope-profile-preview > span { display: flex; justify-content: space-between; gap: 0.75rem; }.scope-profile-preview b, .scope-trace-preview b { font-weight: 400; color: var(--scope-text); }.scope-trace-preview b { margin-left: 0.6rem; } +.scope-paths { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; } +.scope-path { display: flex; flex-direction: column; padding: 1.6rem; color: var(--scope-text); border: 1px solid var(--scope-border); background: var(--scope-surface); border-radius: 0.7rem; transition: border-color 180ms; } +.scope-path-top { display: flex; justify-content: space-between; gap: 1rem; font: 0.65rem var(--sl-font-mono); color: var(--scope-muted); margin-bottom: 1.6rem; } +.scope-path-top > span:first-child { color: var(--scope-brand); } +.scope-path h3 { font-size: 1.1rem; font-weight: 600; letter-spacing: -0.02em; } +.scope-path p { color: var(--scope-muted); font-size: 0.85rem; margin: 0.7rem 0 1.5rem; } +.scope-path .scope-text-link { margin-top: auto; } +.scope-help-row { display: flex; flex-wrap: wrap; gap: 1.25rem; margin-top: 1.5rem; font-size: 0.75rem; } +.scope-help-row span { color: var(--scope-muted); }.scope-help-row a { color: var(--scope-brand); }.scope-help-row a:hover { text-decoration: underline; } +/* Community teaser: restyle the shared article and talk components with landing tokens. */ +.scope-community__grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 2rem; align-items: start; } +.scope-community__label { margin: 0; color: var(--scope-muted); font: 600 0.65rem var(--sl-font-mono); letter-spacing: 0.12em; text-transform: uppercase; } +.scope-community .community-articles { border-top-color: var(--scope-border); } +.scope-community .community-article { border-bottom-color: var(--scope-border); } +.scope-community .community-article__title, .scope-community .community-article__authors { color: var(--scope-text); } +.scope-community .community-article__title:hover { color: var(--scope-brand); } +.scope-community .community-article__meta { color: var(--scope-muted); } +.scope-community .community-talk { margin-top: 1rem; border-color: var(--scope-border); border-radius: 0.8rem; background: var(--scope-surface); } +.scope-community .community-talk__media { background: var(--scope-inset); } +.scope-community .community-talk__title { font-size: 1.1rem; color: var(--scope-text); } +.scope-community .community-talk__speakers { color: var(--scope-text); } +.scope-community .community-talk__meta { color: var(--scope-muted); } +.scope-community .community-talk__meta a { color: var(--scope-brand); } +.scope-community__all { margin-top: 1.5rem; } +.scope-cta { margin-top: 6rem; text-align: center; padding: 4rem 1.5rem; background: radial-gradient(ellipse at 50% 110%, color-mix(in srgb, var(--scope-brand) 12%, transparent), transparent 70%), var(--scope-surface); border: 1px solid var(--scope-border); border-radius: 1rem; } +.scope-cta h2 { font-size: clamp(2rem, 4vw, 3.25rem); letter-spacing: -0.045em; font-weight: 600; line-height: 1.15; margin: 1rem 0; } +.scope-cta > p:not(.scope-eyebrow) { color: var(--scope-muted); font-size: 0.9rem; margin-bottom: 1.5rem; } +.scope-access-note { display: block; font-size: 0.72rem; color: var(--scope-muted); margin: 1rem auto 0; max-width: 50ch; } +.scope-home-footer { display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; padding: 2rem 0 0; font-size: 0.75rem; font-weight: 600; }.scope-home-footer a { color: var(--scope-muted); }.scope-footer-muted { color: var(--scope-muted); font-weight: 400; } + +@media (max-width: 65rem) { + .flow-step-name { display: none; } + .flow-agent { grid-template-columns: 1fr; }.flow-agent-initial { display: none; } + .flow-node { padding: 0.75rem; } + .scope-evidence-grid { grid-template-columns: 1fr 1fr; }.scope-evidence--wide { grid-column: 1 / -1; }.scope-mini-dag { min-height: 80px; } +} +@media (max-width: 48rem) { + .scope-section-heading { display: block; }.scope-section-heading > p, .scope-section-heading > a { display: block; margin-top: 1rem; max-width: 55ch; } + .scope-community__grid { grid-template-columns: 1fr; } + .scope-agent-strip { column-gap: 1rem; row-gap: 0.6rem; }.scope-agent-strip > span:first-child { width: 100%; } + .flow-toolbar { flex-wrap: wrap; padding: 1rem; } + .scope-home .flow-support { padding-inline: 1rem; } + .flow-gate-graph { grid-template-columns: 1fr; } + .flow-gate-group:not(:last-child)::after { top: 100%; left: 50%; width: 0; height: 1.5rem; border-top: 0; border-left: 2px solid var(--scope-brand); } + .flow-canvas { padding: 1rem; }.flow-canvas-label { font-size: 0.6rem; flex-wrap: wrap; } + .flow-connections { display: none; } + .flow-nodes { grid-template-columns: 1fr; gap: 1.4rem; height: auto; } + .flow-node { position: relative; padding: 1rem; } + .flow-node:not(.flow-agent):not(.flow-report)::after, .flow-agent-nodes::after { content: ''; position: absolute; width: 2px; height: 1.4rem; background: var(--scope-brand); bottom: -1.4rem; left: 50%; } + .flow-agent-nodes { position: relative; grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: none; gap: 0.75rem; height: auto; } + .flow-agent { display: grid; grid-template-columns: 24px 1fr; min-height: 88px; }.flow-agent-initial { display: grid; } + .flow-agent:last-child:nth-child(odd) { grid-column: 1 / -1; } + .flow-scenario, .flow-judge, .flow-report { display: grid; grid-template-columns: 32px 1fr; align-items: center; column-gap: 0.8rem; } + .flow-node-icon { grid-row: 1 / 3; margin: 0; }.flow-node:not(.flow-agent) > span:not(.flow-node-icon):not(.flow-node-kicker), .flow-node-tag { grid-column: 2; } + .flow-criteria { flex-direction: row; flex-wrap: wrap; align-items: center; }.flow-criteria i { width: 8px; height: 1px; margin: 0; } + .flow-chart { height: 28px; max-width: 140px; } + .flow-controls { padding: 0.75rem 1rem; gap: 0.5rem; }.flow-step-buttons { gap: 0.15rem; }.flow-step-buttons button { padding: 0.4rem; min-width: 36px; } + .flow-inspector { grid-template-columns: 1fr; gap: 1rem; padding: 1.25rem; min-height: 340px; } + .scope-home .flow-table-hint { display: block; margin-bottom: 0.75rem; color: var(--scope-brand); font: 0.75rem/1.6 var(--sl-font); } + .scope-paths { grid-template-columns: 1fr; }.scope-path-top { margin-bottom: 1rem; } + .scope-cta { margin-top: 3.5rem; } +} +@media (max-width: 30rem) { + .scope-hero { padding-inline: 0; }.scope-hero h1 { font-size: 2.65rem; }.scope-hero > .scope-eyebrow { letter-spacing: 0.07em; font-size: 0.6rem; } + scope-showreel { margin-top: 2.25rem; } + .scope-showreel-controls { position: static; padding: 0.5rem; border-top: 1px solid #ffffff1f; } + .scope-showreel-controls:not(:has(> :not([hidden]))) { display: none; } + .scope-evidence-grid { grid-template-columns: 1fr; }.scope-evidence--wide { grid-column: auto; } + .flow-controls { flex-wrap: wrap; }.flow-step-buttons { width: 100%; justify-content: space-between; }.flow-step-buttons button { min-width: 44px; }.flow-step-name { display: inline; } + .flow-agent { grid-template-columns: 1fr; }.flow-agent-initial { display: none; } + .flow-criteria { flex-direction: column; align-items: start; }.flow-criteria i { width: 1px; height: 9px; margin-left: 0.8rem; } + .flow-demo-label { font-size: 0.75rem; } + .scope-actions { flex-direction: column; }.scope-actions .scope-btn { width: 100%; } } - -.scope-lane__badge { - flex-shrink: 0; - font-family: var(--sl-font-mono, monospace); - font-size: 0.6rem; - font-weight: 700; - letter-spacing: 0.06em; - padding: 0.2rem 0.5rem; - border-radius: 999px; - border: 1px solid var(--border-strong); - color: var(--text-muted); - text-transform: uppercase; -} - -.scope-crit { - display: flex; - align-items: center; - gap: var(--sp-2); - padding: 0.4rem 0; - font-size: 0.82rem; - color: var(--text-muted); -} - -.scope-mark { - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - width: 1.1rem; - height: 1.1rem; - border-radius: 999px; - font-size: 0.68rem; - font-weight: 800; -} -.scope-mark--pass { background: color-mix(in oklch, var(--ok) 16%, transparent); color: var(--ok); } -.scope-mark--fail { background: color-mix(in oklch, var(--fail) 16%, transparent); color: var(--fail); } -.scope-mark--run { background: color-mix(in oklch, var(--brand) 16%, transparent); color: var(--brand); } -.scope-mark--wait { background: var(--surface-2); color: var(--text-faint); border: 1px solid var(--border); } - -@media (max-width: 46rem) { - .scope-board { grid-template-columns: 1fr; } - .scope-lane { border-right: none; border-bottom: 1px solid var(--border); } - .scope-lane:last-child { border-bottom: none; } -} - -/* ================================================================ * - * 5. SECTIONS - * ================================================================ */ - -.scope-section { margin-top: clamp(4rem, 9vw, 7rem); } - -.scope-section__head { max-width: 46rem; margin: 0 0 var(--sp-12); } - -.scope-section__eyebrow { - font-family: var(--sl-font-mono, monospace); - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.16em; - text-transform: uppercase; - color: var(--brand); -} - -.scope-section__head h2 { - margin: var(--sp-3) 0 0; - border: none; - font-size: clamp(1.8rem, 4.2vw, 2.5rem); - letter-spacing: -0.025em; - font-weight: 700; - color: var(--text); -} - -.scope-section__head p { - margin: var(--sp-4) 0 0; - font-size: 1.05rem; - line-height: 1.6; - color: var(--text-muted); -} - -/* ---------------- How it works (steps) ---------------- */ - -.scope-steps { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: var(--sp-4); - counter-reset: step; -} - -.scope-step { - position: relative; - padding: var(--sp-6); - border: 1px solid var(--border); - border-radius: var(--r-lg); - background: var(--surface); -} - -.scope-step__k { - display: inline-flex; - align-items: center; - justify-content: center; - width: 2.1rem; - height: 2.1rem; - border-radius: var(--r-sm); - background: color-mix(in oklch, var(--brand) 14%, transparent); - color: var(--brand); - font-family: var(--sl-font-mono, monospace); - font-weight: 700; - font-size: 0.85rem; -} - -.scope-step h3 { margin: var(--sp-4) 0 var(--sp-2); font-size: 1.1rem; font-weight: 600; color: var(--text); } -.scope-step p { margin: 0; font-size: var(--sl-text-sm); line-height: 1.6; color: var(--text-muted); } - -@media (max-width: 52rem) { .scope-steps { grid-template-columns: 1fr; } } - -/* ---------------- Feature grid ---------------- */ - -.scope-features { - display: grid; - grid-template-columns: repeat(3, 1fr); - border-top: 1px solid var(--border); - border-left: 1px solid var(--border); - border-radius: var(--r-lg); - overflow: hidden; -} - -.scope-feature { - padding: 1.7rem 1.6rem; - border-right: 1px solid var(--border); - border-bottom: 1px solid var(--border); - transition: background 0.15s ease; -} -.scope-feature:hover { background: var(--surface-2); } - -.scope-feature__no { - font-family: var(--sl-font-mono, monospace); - font-size: 0.78rem; - font-weight: 600; - color: var(--brand); -} - -.scope-feature h3 { margin: 0.8rem 0 0.4rem; font-size: 1.08rem; font-weight: 600; letter-spacing: -0.01em; color: var(--text); } -.scope-feature p { margin: 0; font-size: var(--sl-text-sm); line-height: 1.6; color: var(--text-muted); } - -@media (max-width: 60rem) { .scope-features { grid-template-columns: 1fr 1fr; } } -@media (max-width: 38rem) { .scope-features { grid-template-columns: 1fr; } } - -/* ---------------- Channels ---------------- */ - -.scope-channels { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--sp-4); } - -.scope-channel { - display: flex; - flex-direction: column; - min-width: 0; - padding: var(--sp-6); - border: 1px solid var(--border); - border-radius: var(--r-lg); - background: var(--surface); - transition: border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; -} -.scope-channel:hover { - border-color: var(--border-strong); - transform: translateY(-2px); - box-shadow: 0 18px 40px -28px rgba(15, 23, 42, 0.5); -} - -.scope-channel__k { - font-family: var(--sl-font-mono, monospace); - font-size: 0.72rem; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--text-faint); -} -.scope-channel h3 { margin: 0.5rem 0; font-size: 1.25rem; font-weight: 600; color: var(--text); } -.scope-channel p { margin: 0 0 var(--sp-4); font-size: var(--sl-text-sm); line-height: 1.6; color: var(--text-muted); flex: 1; } -.scope-channel a { font-size: var(--sl-text-sm); font-weight: 600; color: var(--brand); text-decoration: none; } -.scope-channel a:hover { color: var(--brand-strong); } - -@media (max-width: 60rem) { .scope-channels { grid-template-columns: 1fr; } } - -/* ---------------- Explore-the-docs cards ---------------- */ - -.scope-home .card-grid { gap: var(--sp-4); } -.scope-home .card { - border-color: var(--border); - background: var(--surface); - box-shadow: none; - transition: border-color 0.15s ease, transform 0.15s ease; -} -.scope-home .card:hover { border-color: var(--border-strong); transform: translateY(-2px); } -.scope-home .card .title { color: var(--text); font-weight: 600; } -.scope-home .card a { color: var(--brand); } - -/* ---------------- Community (articles & talks) ---------------- */ - -.scope-community__grid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: var(--sp-8); - align-items: start; -} -.scope-community__grid > * { margin-top: 0; } - -.scope-community__label { - margin: 0; - font-family: var(--sl-font-mono, monospace); - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--text-faint); -} - -.scope-community .community-articles { border-top-color: var(--border); } -.scope-community .community-article { border-bottom-color: var(--border); } -.scope-community .community-article__title { color: var(--text); } -.scope-community .community-article__title:hover { color: var(--brand); } -.scope-community .community-article__authors { color: var(--text); } -.scope-community .community-article__meta { color: var(--text-muted); } - -.scope-community .community-talk { - margin-top: var(--sp-4); - border-color: var(--border); - border-radius: var(--r-lg); - background: var(--surface); -} -.scope-community .community-talk__media { background: var(--surface-2); } -.scope-community .community-talk__title { font-size: 1.1rem; color: var(--text); } -.scope-community .community-talk__speakers { color: var(--text); } -.scope-community .community-talk__meta { color: var(--text-muted); } -.scope-community .community-talk__meta a { color: var(--brand); } - -.scope-community__all { - display: inline-block; - margin-top: var(--sp-8); - font-size: var(--sl-text-sm); - font-weight: 600; - color: var(--brand); - text-decoration: none; -} -.scope-community__all:hover { color: var(--brand-strong); } - -@media (max-width: 52rem) { .scope-community__grid { grid-template-columns: 1fr; } } - -/* ================================================================ * - * 6. CTA - * ================================================================ */ - -.scope-cta { - margin-top: clamp(4rem, 9vw, 7rem); - border: 1px solid var(--border); - border-radius: var(--r-xl); - padding: clamp(2.5rem, 6vw, 4.5rem) var(--sp-6); - text-align: center; - background: var(--surface-2); -} - -.scope-cta h2 { - margin: 0 auto; - border: none; - max-width: 20ch; - font-size: clamp(1.7rem, 4.2vw, 2.5rem); - font-weight: 700; - letter-spacing: -0.025em; - color: var(--text); -} - -.scope-cta p { margin: var(--sp-4) auto 0; max-width: 42ch; line-height: 1.6; color: var(--text-muted); } -.scope-cta .scope-actions { margin-top: var(--sp-8); } - -/* ================================================================ * - * 7. Motion discipline - * ================================================================ */ - @media (prefers-reduced-motion: reduce) { - .scope-home * { transition: none !important; } + .scope-home *, .scope-home *::before, .scope-home *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; } } diff --git a/website/vitest.config.ts b/website/vitest.config.ts new file mode 100644 index 000000000..b75eadb9b --- /dev/null +++ b/website/vitest.config.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + root: fileURLToPath(new URL('.', import.meta.url)), + test: { + include: ['src/scripts/**/*.test.ts'], + environment: 'node', + }, +});