1---
2import Marketing from "../layouts/Marketing.astro";
3import CodeBlock from "../components/marketing/CodeBlock.astro";
4import Button from "../components/marketing/Button.astro";
5import ProviderMark from "../components/marketing/ProviderMark.astro";
6import HeroBgGraph from "../components/marketing/HeroBgGraph.astro";
7import Yantra from "../brand/Yantra.astro";
8import DevTuiTerminal from "../components/marketing-islands/DevTuiTerminal.tsx";
9import DeployTerminal from "../components/marketing-islands/DeployTerminal.tsx";
10import HeroDeployTerminal from "../components/marketing-islands/HeroDeployTerminal.tsx";
11import Sentiment from "../components/marketing/Sentiment.astro";
12import AlchemicalDivider from "../components/marketing/AlchemicalDivider.astro";
13import HeroCodeBlock from "../components/marketing/HeroCodeBlock.astro";
14import TestTerminal from "../components/marketing-islands/TestTerminal.tsx";
15import PRLifecycle from "../components/marketing-islands/PRLifecycle.tsx";
16import CopyForAgent from "../components/marketing-islands/CopyForAgent.tsx";
17
18// Hero: the smallest complete-looking Worker (KV only; `Sessions` and
19// `Bindings` are declared elsewhere in the file).
20const HERO_SRC = `export default Cloudflare.Worker(
21 "Api",
22 { main: import.meta.url },
23 Effect.gen(function* () {
24 const kv = yield* Cloudflare.KV.ReadWriteNamespace(Sessions);
25 return {
26 fetch: Effect.gen(function* () {
27 const user = yield* kv.get("user");
28 return yield* HttpServerResponse.json({ user });
29 }),
30 };
31 }).pipe(Effect.provide(Bindings)),
32);`;
33
34// Deploy section: the whole Worker file, minus imports.
35const STACK_SRC = `export const Photos = Cloudflare.R2.Bucket("Photos");
36export const Sessions = Cloudflare.KV.Namespace("Sessions");
37
38const Bindings = Layer.mergeAll(
39 Cloudflare.R2.ReadWriteBucketBinding,
40 Cloudflare.KV.ReadWriteNamespaceBinding,
41);
42
43export default Cloudflare.Worker(
44 "Api",
45 { main: import.meta.url },
46 Effect.gen(function* () {
47 const photos = yield* Cloudflare.R2.ReadWriteBucket(Photos);
48 const kv = yield* Cloudflare.KV.ReadWriteNamespace(Sessions);
49 return {
50 fetch: Effect.gen(function* () {
51 const { objects } = yield* photos.list();
52 const user = yield* kv.get("user");
53 return yield* HttpServerResponse.json({ user, objects });
54 }),
55 };
56 }).pipe(Effect.provide(Bindings)),
57);`;
58
59// The hero terminal covers the code card's empty bottom-right corner: it
60// starts below the last line that reaches TERM_COL, so no code is hidden.
61const TERM_COL = 38;
62const STACK_LINES = HERO_SRC.split("\n");
63const TERM_LINE =
64 STACK_LINES.findLastIndex((l) => l.trimEnd().length > TERM_COL - 2) + 1;
65const STACK_WIDTH = Math.max(...STACK_LINES.map((l) => l.length));
66
67const TEST_SRC = `// each suite deploys its own isolated stack
68const stack = beforeAll(deploy(Stack)); // real R2, real DynamoDB
69afterAll(destroy(Stack)); // torn down at the end
70
71test("PUT + GET round-trips through R2", Effect.gen(function* () {
72 const { url } = yield* stack;
73 const res = yield* HttpClient.get(\`\${url}/object/hello.txt\`);
74 expect(yield* res.text).toBe("hi!");
75}));`;
76
77const OTEL_SRC = `const requestsTotal = Metric.counter("requests_total");
78
79// any OTLP endpoint: Axiom, Datadog, Honeycomb, a collector…
80const Otel = Alchemy.Telemetry.layerOtlp({
81 url: "https://otlp.example.com",
82});
83
84export default Cloudflare.Worker(
85 "Api",
86 { main: import.meta.url },
87 Effect.gen(function* () {
88 yield* Effect.logInfo("request received");
89 yield* Metric.increment(requestsTotal);
90 return { fetch: handler };
91 }).pipe(Effect.provide(Otel)),
92);`;
93
94const DASH_SRC = `export const Dashboard = AWS.CloudWatch.Dashboard("ApiHealth", {
95 widgets: [
96 Widget.line({ title: "p99 latency", metric: api.metrics.p99 }),
97 Widget.line({ title: "requests/s", metric: api.metrics.rps }),
98 Widget.number({ title: "5xx", metric: api.metrics.errorRate }),
99 ],
100});
101
102export const P99Alarm = AWS.CloudWatch.Alarm("p99Latency", {
103 metric: api.metrics.p99,
104 threshold: 500,
105 alarmActions: [pagerDuty, slackWebhook],
106});`;
107
108const VITE_SRC = `import Api from "./src/api.ts";
109
110export default Alchemy.Stack(
111 "my-app",
112 { providers: Cloudflare.providers() },
113 Effect.gen(function* () {
114 // backend: a Cloudflare Worker, run locally in workerd
115 const api = yield* Api;
116 // frontend: a Vite app that calls it
117 const web = yield* Cloudflare.Website.Vite("Web", {
118 env: { VITE_API_URL: api.url },
119 });
120 return { web: web.url, api: api.url };
121 }),
122);`;
123
124// Marks render monochrome (inheriting the label color) and take on the
125// official brand hue only on hover. `color` values are verified against
126// svgl.app's SVGs; Prisma swaps to its multi-color mark instead.
127const PROVIDERS = [
128 { label: "Cloudflare", icon: "Cloudflare", href: "/cloudflare", color: "#f4811f" },
129 { label: "AWS", icon: "AWS", href: "/aws", color: "#ff9900" },
130 { label: "Hetzner", icon: "Hetzner", href: "/hetzner", color: "#d50c2d" },
131 { label: "Fly", icon: "Fly", href: "/fly", color: "#996bec" },
132 { label: "Railway", icon: "Railway", href: "/railway" },
133 { label: "Neon", icon: "Neon", href: "/neon", color: "#37c38f" },
134 { label: "PlanetScale", icon: "PlanetScale", href: "/planetscale" },
135 { label: "Prisma", icon: "Prisma", iconHover: "PrismaColor", href: "/prisma" },
136 { label: "Docker", icon: "Docker", href: "/docker", color: "#008fe2" },
137 { label: "Stripe", icon: "Stripe", href: "/stripe", color: "#533afd" },
138 { label: "GitHub", icon: "GitHub", href: "/github" },
139 { label: "Axiom", icon: "Axiom", href: "/axiom" },
140];
141
142---
143
144<Marketing
145 title="Alchemy — reach for the better primitives."
146 description="Snap together cloud resources in TypeScript and let Alchemy take care of the wiring — permissions, config, and bindings — so you can focus on your product."
147>
148 <!-- Hero + marquee fill the first screen so the marquee lands just above the fold. -->
149 <div class="v2-fold">
150 <!-- ───────────────── HERO ───────────────── -->
151 <section class="alc-hero v2-hero" style="padding:20px 32px 16px;">
152 <HeroBgGraph />
153 <button type="button" class="v2-motion-toggle" id="motion-toggle" aria-pressed="false">
154 <span class="v2-motion-toggle__label">Pause motion</span>
155 </button>
156 <div class="v2-hero__grid">
157 <div class="v2-hero__copy">
158 <div class="alc-hero__inner">
159 <Yantra class="alc-hero__mark v2-hero__mark" size={42} />
160 <h1
161 data-copy
162 class="alc-hero__title"
163 id="hero-h1"
164 style="font-size:clamp(38px,6.4vw,60px);"
165 >
166 Reach for the better primitives
167 </h1>
168 <p
169 data-copy
170 class="alc-hero__subtitle"
171 id="hero-sub"
172 style="font-size:clamp(17px,2.2vw,22px);max-width:560px;"
173 >
174 Snap together cloud resources and let Alchemy do the wiring so you can focus on your product.
175 </p>
176 <div class="alc-hero__cta">
177 <Button variant="primary" icon="arrow" href="/getting-started">
178 <span data-copy>Start your project</span>
179 </Button>
180 <Button variant="secondary" href="#agent-prompt">
181 <span data-copy>Build it with your agent</span>
182 <span class="v2-hero__cta-arrow" aria-hidden="true">&darr;</span>
183 </Button>
184 </div>
185 </div>
186 </div>
187 <div
188 class="v2-hero__visual"
189 aria-hidden="true"
190 style={`--term-line:${TERM_LINE};--term-col:${TERM_COL}ch;--code-cols:${STACK_WIDTH + 1}ch`}
191 >
192 <div class="v2-hero__code">
193 <HeroCodeBlock filename="src/api.ts" code={HERO_SRC} compact />
194 </div>
195 <div class="v2-hero__term">
196 <HeroDeployTerminal client:load />
197 </div>
198 </div>
199 </div>
200 </section>
201
202 <!-- ───────────────── PROVIDER MARQUEE ───────────────── -->
203 <div class="v2-marquee" aria-label="Supported cloud providers">
204 <div class="v2-marquee__track">
205 {
206 /* Four copies so the -50% loop shift always lands on an identical
207 frame with content still covering viewports up to ~2 copies wide.
208 Two copies leave a blank tail on screens wider than one copy. */
209 [0, 1, 2, 3].map((dup) => (
210 <ul class="v2-marquee__group" aria-hidden={dup > 0}>
211 {PROVIDERS.map((p) => (
212 <li>
213 <a
214 class="v2-marquee__item"
215 href={p.href}
216 tabindex={dup > 0 ? -1 : undefined}
217 style={"color" in p ? `--brand:${p.color}` : undefined}
218 >
219 <span
220 class:list={[
221 "v2-marquee__mark",
222 "iconHover" in p && "v2-marquee__mark--swap",
223 ]}
224 >
225 <ProviderMark name={p.icon} size={17} />
226 </span>
227 {"iconHover" in p && (
228 <span class="v2-marquee__mark v2-marquee__mark--hover">
229 <ProviderMark name={p.iconHover} size={17} />
230 </span>
231 )}
232 {p.label}
233 </a>
234 </li>
235 ))}
236 </ul>
237 ))
238 }
239 </div>
240 </div>
241 </div>
242
243 <script>
244 // WCAG 2.2.2: the hero rings and the provider marquee move on their
245 // own, so offer a way to stop them. Remembered across visits.
246 const KEY = "alc-motion-paused";
247 const btn = document.getElementById("motion-toggle")!;
248 const label = btn.querySelector(".v2-motion-toggle__label")!;
249 const apply = (paused: boolean) => {
250 document.documentElement.classList.toggle("alc-motion-paused", paused);
251 btn.setAttribute("aria-pressed", String(paused));
252 label.textContent = paused ? "Play motion" : "Pause motion";
253 window.dispatchEvent(new Event("alc-motion-change"));
254 };
255 apply(localStorage.getItem(KEY) === "1");
256 btn.addEventListener("click", () => {
257 const paused = !document.documentElement.classList.contains("alc-motion-paused");
258 localStorage.setItem(KEY, paused ? "1" : "0");
259 apply(paused);
260 });
261 </script>
262
263 <div class="v2-scroll-hint" id="scroll-hint" aria-hidden="true">
264 <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6" /></svg>
265 </div>
266 <script>
267 // Only while scrolled to the top, and only when the hero + marquee fit
268 // on screen (the chevron sits in the gap below the marquee).
269 const hint = document.getElementById("scroll-hint")!;
270 const fold = document.querySelector<HTMLElement>(".v2-fold")!;
271 const sync = () =>
272 hint.classList.toggle(
273 "is-hidden",
274 scrollY > 4 || fold.getBoundingClientRect().bottom > innerHeight + 1,
275 );
276 addEventListener("scroll", sync, { passive: true });
277 addEventListener("resize", sync);
278 sync();
279 </script>
280
281 <!-- ───────────────── THE FULL PICTURE ───────────────── -->
282 <section class="v2-section v2-pat-1 v2-overview">
283 <div class="v2-container">
284 <h2 class="v2-h2" data-copy>The full picture</h2>
285 <p class="v2-lede" data-copy>
286 Alchemy can deploy, test, and destroy your app. That's everything you need to have a full CI experience with branch previews and live test on real infratructure. Oh and the graphic below is how long it actually takes; Alchemy does all of that in ~25 seconds.
287 </p>
288 <p class="v2-overview__source">
289 <a
290 href="https://github.com/alchemy-run/alchemy/tree/main/examples/cloudflare-preview-benchmark"
291 target="_blank"
292 rel="noopener"
293 >
294 <span data-copy>See the example app and benchmark on GitHub</span>
295 <span aria-hidden="true">&rarr;</span>
296 </a>
297 </p>
298 <PRLifecycle client:visible />
299 </div>
300 </section>
301
302 <!-- ───────────────── DEV ───────────────── -->
303 <AlchemicalDivider variant="triangle" class="v2-divider" />
304 <section class="v2-section v2-pat-2 v2-feature">
305 <div class="v2-container">
306 <h2 class="v2-h2" data-copy>Run your entire stack locally</h2>
307 <p class="v2-lede" data-copy>
308 As apps grow you have more and more services; each backend service needs to be started in a local emulator or proxy a live service, each client needs its own dev server, and you have to set up HMR. <code class="alc-code-inline">alchemy dev</code> manages all of that for you, 1 command, no extra configuration, and all your services just start.
309 </p>
310 <div class="v2-split">
311 <CodeBlock filename="alchemy.run.ts" code={VITE_SRC} compact />
312 <DevTuiTerminal client:visible />
313 </div>
314 </div>
315 </section>
316
317 <!-- ───────────────── TESTING ───────────────── -->
318 <AlchemicalDivider variant="square" class="v2-divider" />
319 <section class="v2-section v2-pat-3 v2-feature">
320 <div class="v2-container">
321 <h2 class="v2-h2" data-copy>Run tests against real infrastructre</h2>
322 <p class="v2-lede" data-copy>
323 <code class="alc-code-inline">alchemy test</code> deploys a temporary stack, runs your tests against it, then tears it down. This way tests hit a real environment instead of mocks or local emulation, and agent loops stay accurate. In CI, tests can inherit the preview deployment stack so deploy doesn't run twice.
324 </p>
325 <div class="v2-split">
326 <CodeBlock filename="test/api.test.ts" code={TEST_SRC} compact />
327 <TestTerminal client:visible />
328 </div>
329 </div>
330 </section>
331
332 <!-- ───────────────── PLAN & DEPLOY ───────────────── -->
333 <AlchemicalDivider variant="pentagon" class="v2-divider" />
334 <section class="v2-section v2-pat-4 v2-feature">
335 <div class="v2-container">
336 <h2 class="v2-h2" data-copy>Dry run before you deploy</h2>
337 <p class="v2-lede" data-copy>
338 <code class="alc-code-inline">plan</code> does a dry-run and show what will change.<code class="alc-code-inline">deploy</code> updates your infrastructure to match the desired state. <code class="alc-code-inline">destroy</code> tears it all down (with safe guards to prevent accidental data deletion). Simple enough for agents, easy enough you'll choose to use it too
339 </p>
340 <div class="v2-split">
341 <CodeBlock filename="src/api.ts" code={STACK_SRC} compact />
342 <DeployTerminal client:visible />
343 </div>
344 </div>
345 </section>
346
347 <!-- ───────────────── OBSERVABILITY ───────────────── -->
348 <AlchemicalDivider variant="hexagon" class="v2-divider" />
349 <section class="v2-section v2-pat-5 v2-feature">
350 <div class="v2-container">
351 <h2 class="v2-h2" data-copy>Wired for observability</h2>
352 <p class="v2-lede" data-copy>
353 Alchemy has logs, spans, and metrics set up by default. The exporter is an Effect Layer so changing providers is a 1 line swap, whether its Axiom, Datadog, or any other OTLP endpoint.<br>Dashboards and alarms are resources themselve so you can define them right next to the resources they are responsible for.
354 </p>
355 <div class="v2-split">
356 <CodeBlock filename="src/Api.ts" code={OTEL_SRC} compact />
357 <CodeBlock filename="alchemy.run.ts" code={DASH_SRC} compact />
358 </div>
359 </div>
360 </section>
361
362 <!-- ───────────────── SENTIMENT ───────────────── -->
363 <AlchemicalDivider variant="heptagon" class="v2-divider" />
364 <section class="v2-section v2-pat-1" aria-labelledby="sentiment-h">
365 <div class="v2-container">
366 <h2 class="v2-h2" id="sentiment-h" data-copy>Loved by the people shipping with it</h2>
367 <p class="v2-lede" data-copy>
368 What builders say after putting Alchemy in front of their agents,
369 their Cloudflare apps, and their production infrastructure.
370 </p>
371 <Sentiment />
372 </div>
373 </section>
374
375 <!-- ───────────────── GET STARTED ───────────────── -->
376 <AlchemicalDivider variant="circle" class="v2-divider" />
377 <section class="v2-section v2-pat-6 v2-section--last">
378 <div class="v2-container">
379 <h2 class="v2-h2" data-copy>Get started.</h2>
380 <ol class="v2-steps">
381 <li class="v2-steps__inline">
382 <span class="v2-steps__label" data-copy>Install</span>
383 <code class="v2-steps__code">pnpm add alchemy@latest effect</code>
384 </li>
385 <li id="agent-prompt">
386 <span class="v2-steps__label" data-copy>Paste this into your coding agent</span>
387 <CopyForAgent client:load />
388 </li>
389 <li class="v2-steps__inline">
390 <span class="v2-steps__label" data-copy>Ship</span>
391 <code class="v2-steps__code">pnpm alchemy deploy</code>
392 </li>
393 </ol>
394 <div class="v2-cta-buttons">
395 <Button variant="secondary" icon="arrow" href="/getting-started">
396 <span data-copy>Read the tutorial</span>
397 </Button>
398 <Button
399 variant="ghost"
400 icon="github"
401 href="https://github.com/alchemy-run/alchemy"
402 >
403 <span data-copy>GitHub</span>
404 </Button>
405 </div>
406 </div>
407 </section>
408</Marketing>
409
410<style>
411 /* ---------- first screen: hero + marquee, fold just below the marquee ---------- */
412 .v2-fold {
413 display: flex;
414 flex-direction: column;
415 /* 64px sticky header, ~32px of the next section peeking below the marquee. */
416 min-height: max(620px, calc(100svh - 64px - 32px));
417 }
418 .v2-fold > .v2-hero {
419 flex: 1;
420 display: flex;
421 flex-direction: column;
422 justify-content: center;
423 }
424 .v2-fold > .v2-hero > .v2-hero__grid {
425 width: 100%;
426 }
427 .v2-motion-toggle {
428 position: absolute;
429 right: 20px;
430 top: 12px;
431 z-index: 3;
432 padding: 4px 10px;
433 border: 1px solid var(--alc-hairline-2);
434 border-radius: 999px;
435 background: color-mix(in srgb, var(--alc-bg) 70%, transparent);
436 color: var(--alc-fg-3);
437 font: 500 12px var(--alc-font-sans);
438 cursor: pointer;
439 min-height: 24px;
440 }
441 .v2-motion-toggle:hover {
442 color: var(--alc-fg-1);
443 border-color: var(--alc-hairline-3);
444 }
445 .v2-motion-toggle:focus-visible {
446 outline: 2px solid var(--alc-accent);
447 outline-offset: 2px;
448 }
449 /* Nothing moves under reduced motion, so the control isn't needed. */
450 @media (prefers-reduced-motion: reduce) {
451 .v2-motion-toggle {
452 display: none;
453 }
454 }
455 .v2-scroll-hint {
456 position: fixed;
457 left: 50%;
458 bottom: 6px;
459 z-index: 5;
460 translate: -50% 0;
461 color: var(--alc-fg-2);
462 opacity: 0.35;
463 pointer-events: none;
464 transition: opacity 300ms ease;
465 animation: v2-scroll-hint-pulse 2.2s ease-in-out infinite;
466 }
467 .v2-scroll-hint svg {
468 display: block;
469 }
470 .v2-scroll-hint.is-hidden {
471 opacity: 0;
472 animation: none;
473 }
474 @keyframes v2-scroll-hint-pulse {
475 0%,
476 100% {
477 opacity: 0.15;
478 transform: translateY(-3px);
479 }
480 50% {
481 opacity: 0.45;
482 transform: translateY(2px);
483 }
484 }
485 @media (prefers-reduced-motion: reduce) {
486 .v2-scroll-hint {
487 animation: none;
488 }
489 }
490
491 /* ---------- hero: copy left, deploy visual right ---------- */
492 .v2-hero__grid {
493 position: relative;
494 z-index: 1;
495 max-width: 1180px;
496 margin: 0 auto;
497 display: grid;
498 grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.3fr);
499 gap: 48px;
500 align-items: center;
501 }
502 .v2-hero__copy :global(.alc-hero__inner) {
503 text-align: left;
504 margin: 0;
505 }
506 /* Shrink-wrap the mark so its box center is the glyph center: the
507 background graph measures this element to place its vanishing point. */
508 .v2-hero__copy :global(.alc-hero__mark) {
509 width: fit-content;
510 margin: 0 0 22px;
511 }
512 .v2-hero__copy :global(.alc-hero__subtitle) {
513 margin-inline: 0;
514 }
515 .v2-hero__copy :global(.alc-hero__cta) {
516 justify-content: flex-start;
517 }
518 /* Hero CTAs use the logo's colors: the leaf-green mark for the main
519 action, and the terracotta dot as the secondary action's accent. */
520 .v2-hero__copy :global(.alc-btn--primary) {
521 background: var(--alc-accent-deep);
522 color: var(--alc-bg);
523 }
524 .v2-hero__copy :global(.alc-btn--primary:hover) {
525 background: var(--alc-accent-bright);
526 }
527 .v2-hero__copy :global(.alc-btn--secondary) {
528 padding: 10px 18px;
529 background: color-mix(in srgb, var(--alc-bg) 60%, transparent);
530 border-color: var(--alc-hairline-3);
531 }
532 .v2-hero__copy :global(.alc-btn--secondary:hover) {
533 border-color: var(--alc-terracotta);
534 }
535 .v2-hero__cta-arrow {
536 color: var(--alc-terracotta);
537 }
538 /* The terminal sits over the code card's empty bottom-right corner.
539 --term-line / --term-col come from HERO_SRC (see TERM_LINE) and are
540 in the code font's units (11px mono, 1.65 line-height). */
541 .v2-hero__visual {
542 --code-lh: 18.15px;
543 --code-header: 36px;
544 /* Code and terminal share one grid cell, so the box grows to fit the
545 terminal's overhang below the code card. */
546 display: grid;
547 align-items: start;
548 justify-items: start;
549 text-align: left;
550 font-family: var(--alc-font-mono);
551 font-size: 11px;
552 }
553 .v2-hero__code :global(.alc-code-block__pre) {
554 font-size: 11px;
555 line-height: 1.65;
556 }
557 .v2-hero__code,
558 .v2-hero__term {
559 grid-area: 1 / 1;
560 }
561 .v2-hero__code {
562 width: calc(34px + var(--code-cols));
563 max-width: 100%;
564 }
565 .v2-hero__term {
566 position: relative;
567 margin-top: calc(var(--code-header) + 12px + var(--term-line) * var(--code-lh) + 8px);
568 margin-left: calc(16px + var(--term-col));
569 z-index: 1;
570 /* Roomier than the longest deploy line (47 chars at 12.5px), capped so
571 it never overhangs the column by more than 24px. */
572 width: min(470px, calc(100% - 16px - var(--term-col) + 24px));
573 box-shadow: 0 18px 48px rgba(0, 0, 0, 0.45);
574 border-radius: 10px;
575 }
576 /* The section-wide halo assumed centered copy; the copy column carries
577 its own below. */
578 .v2-hero::after {
579 background: none;
580 }
581 .v2-hero__copy {
582 position: relative;
583 }
584 /* Fades the rings behind the headline and subtitle so they stay
585 readable; the mark sits near the edge so the rings still read as
586 emanating from it. */
587 .v2-hero__copy::before {
588 content: "";
589 position: absolute;
590 inset: -60px -160px -90px -160px;
591 z-index: -1;
592 pointer-events: none;
593 /* Same stops as the original centered hero's halo (.alc-hero::after),
594 centered on the headline + subtitle. */
595 background: radial-gradient(
596 closest-side at 50% 62%,
597 color-mix(in srgb, var(--alc-bg) 98%, transparent) 0%,
598 color-mix(in srgb, var(--alc-bg) 94%, transparent) 50%,
599 color-mix(in srgb, var(--alc-bg) 68%, transparent) 78%,
600 transparent 100%
601 );
602 }
603 @media (max-width: 1100px) {
604 .v2-hero__grid {
605 grid-template-columns: minmax(0, 1fr);
606 gap: 40px;
607 }
608 .v2-hero__visual {
609 max-width: 700px;
610 }
611 }
612 @media (max-width: 760px) {
613 .v2-hero__visual {
614 justify-items: stretch;
615 }
616 .v2-hero__code {
617 display: none;
618 }
619 .v2-hero__term {
620 margin: 0;
621 width: auto;
622 }
623 }
624
625 .v2-container {
626 max-width: 1080px;
627 margin: 0 auto;
628 }
629
630 #agent-prompt {
631 scroll-margin-top: 96px;
632 }
633
634 /* ---------- provider marquee ---------- */
635 .v2-marquee {
636 overflow: hidden;
637 border-top: 1px solid var(--alc-hairline);
638 padding: 14px 0;
639 -webkit-mask-image: linear-gradient(
640 to right,
641 transparent,
642 #000 8%,
643 #000 92%,
644 transparent
645 );
646 mask-image: linear-gradient(
647 to right,
648 transparent,
649 #000 8%,
650 #000 92%,
651 transparent
652 );
653 }
654 .v2-marquee__track {
655 display: flex;
656 width: max-content;
657 animation: v2-marquee-scroll 72s linear infinite;
658 }
659 .v2-marquee:hover .v2-marquee__track,
660 .v2-marquee:focus-within .v2-marquee__track,
661 :global(html.alc-motion-paused) .v2-marquee__track {
662 animation-play-state: paused;
663 }
664 .v2-marquee__group {
665 display: flex;
666 align-items: center;
667 list-style: none;
668 margin: 0;
669 padding: 0;
670 }
671 .v2-marquee__item {
672 display: inline-flex;
673 align-items: center;
674 gap: 8px;
675 padding: 4px 26px;
676 font-family: var(--alc-font-mono);
677 font-size: 12.5px;
678 letter-spacing: 0.08em;
679 text-transform: uppercase;
680 white-space: nowrap;
681 color: var(--alc-fg-2);
682 text-decoration: none;
683 transition: color 120ms ease;
684 }
685 .v2-marquee__item:hover {
686 color: var(--alc-fg-1);
687 }
688 .v2-marquee__mark {
689 display: inline-flex;
690 align-items: center;
691 /* Optical correction: the labels are uppercase-only, so their visual
692 mass sits above the flex-centered midline; drop the mark to match. */
693 transform: translateY(0.5px);
694 }
695 /* Monochrome at rest; brand color only under the cursor. Prisma swaps
696 between its mono and multi-color marks instead of tinting. */
697 .v2-marquee__item:hover .v2-marquee__mark {
698 color: var(--brand, currentColor);
699 }
700 .v2-marquee__mark--hover {
701 display: none;
702 }
703 .v2-marquee__item:hover .v2-marquee__mark--hover {
704 display: inline-flex;
705 }
706 .v2-marquee__item:hover .v2-marquee__mark--swap {
707 display: none;
708 }
709 .v2-marquee__item :global(svg) {
710 display: block;
711 flex: none;
712 opacity: 0.9;
713 }
714 @keyframes v2-marquee-scroll {
715 from {
716 transform: translateX(0);
717 }
718 to {
719 transform: translateX(-50%);
720 }
721 }
722 @media (prefers-reduced-motion: reduce) {
723 .v2-marquee__track {
724 animation: none;
725 }
726 .v2-marquee {
727 overflow-x: auto;
728 }
729 }
730
731 /* ---------- sections ---------- */
732 .v2-section {
733 padding: 80px 32px;
734 }
735 /* Sections are separated by the original site's geometric dividers:
736 triangle → square → pentagon → hexagon → circle, down the page. */
737 :global(.v2-divider) {
738 margin: 0 auto;
739 }
740 /* A very faint, static echo of the hero's ring geometry behind each
741 section, alternating sides. Masked so it takes the theme's text color. */
742 .v2-section {
743 position: relative;
744 isolation: isolate;
745 overflow: hidden;
746 }
747 .v2-section::before {
748 content: "";
749 position: absolute;
750 top: 50%;
751 right: -380px;
752 z-index: -1;
753 width: 1000px;
754 height: 1000px;
755 translate: 0 -50%;
756 pointer-events: none;
757 background: var(--alc-fg-1);
758 opacity: 0.045;
759 mask: var(--pattern) center / contain no-repeat;
760 -webkit-mask: var(--pattern) center / contain no-repeat;
761 }
762 /* A different Sri Yantra motif per section. */
763 .v2-pat-1 { --pattern: url("/patterns/yantra-1.svg"); }
764 .v2-pat-2 { --pattern: url("/patterns/yantra-2.svg"); }
765 .v2-pat-3 { --pattern: url("/patterns/yantra-3.svg"); }
766 .v2-pat-4 { --pattern: url("/patterns/yantra-4.svg"); }
767 .v2-pat-5 { --pattern: url("/patterns/yantra-5.svg"); }
768 .v2-pat-6 { --pattern: url("/patterns/yantra-6.svg"); }
769 .v2-pat-2::before,
770 .v2-pat-4::before,
771 .v2-pat-6::before {
772 right: auto;
773 left: -380px;
774 }
775 .v2-section--last {
776 padding-bottom: 96px;
777 }
778 .v2-h2 {
779 font-family: var(--alc-font-serif);
780 font-size: clamp(30px, 4.5vw, 40px);
781 line-height: 1.1;
782 letter-spacing: -0.01em;
783 font-weight: 500;
784 color: var(--alc-fg-1);
785 margin: 0 0 14px;
786 }
787 .v2-h3 {
788 font-family: var(--alc-font-serif);
789 font-size: clamp(24px, 3.5vw, 31px);
790 line-height: 1.15;
791 letter-spacing: -0.01em;
792 font-weight: 500;
793 color: var(--alc-fg-1);
794 margin: 8px 0 12px;
795 }
796 .v2-lede {
797 font-size: 16px;
798 line-height: 1.65;
799 color: var(--alc-fg-2);
800 max-width: 680px;
801 margin: 0 0 28px;
802 text-wrap: balance;
803 }
804
805 .v2-split {
806 display: grid;
807 grid-template-columns: 1fr 1fr;
808 gap: 24px;
809 align-items: start;
810 }
811 /* Grid items default to min-width:auto, so a long unwrappable terminal
812 line would stretch its column as the animation plays. Clamp both
813 columns to the track size; overflow stays inside the card. */
814 .v2-split > :global(*) {
815 min-width: 0;
816 }
817 /* Side-by-side cards always match heights: every row item stretches, the
818 island wrappers pass that height down, and the shorter card's body
819 grows (a terminal keeps its designed minimum). Stacked on narrow
820 screens, each card keeps its own height. */
821 @media (min-width: 901px) {
822 .v2-split {
823 align-items: stretch;
824 }
825 .v2-split > :global(*),
826 .v2-split :global(astro-island),
827 .v2-split :global(astro-island > div) {
828 display: flex;
829 flex-direction: column;
830 }
831 .v2-split :global(astro-island > div),
832 .v2-split :global(.alc-code-block),
833 .v2-split :global(.alc-term) {
834 flex: 1;
835 }
836 .v2-split :global(.alc-code-block__pre),
837 .v2-split :global(.alc-term__body) {
838 flex: 1;
839 }
840 .v2-split :global(.alc-term__body) {
841 height: auto !important;
842 }
843 }
844 @media (max-width: 900px) {
845 .v2-split {
846 grid-template-columns: 1fr;
847 }
848 }
849
850 /* ---------- the full picture (PR lifecycle overview) ---------- */
851 .v2-overview {
852 border-top: none;
853 padding-top: 72px;
854 }
855 .v2-overview__source {
856 margin: -14px 0 24px;
857 font-size: 14px;
858 }
859 .v2-overview__source a {
860 display: inline-flex;
861 gap: 6px;
862 color: var(--alc-fg-2);
863 text-decoration: underline;
864 text-decoration-color: var(--alc-hairline-3);
865 text-underline-offset: 3px;
866 }
867 .v2-overview__source a:hover {
868 color: var(--alc-fg-1);
869 text-decoration-color: currentColor;
870 }
871
872 /* ---------- feature sections ---------- */
873 .v2-feature .v2-h2 {
874 margin-bottom: 12px;
875 }
876
877 @media (max-width: 640px) {
878 .v2-section {
879 padding-left: 20px;
880 padding-right: 20px;
881 }
882 }
883
884 /* ---------- get started ---------- */
885 .v2-steps {
886 list-style: none;
887 counter-reset: step;
888 margin: 32px 0 0;
889 padding: 0;
890 display: flex;
891 flex-direction: column;
892 gap: 28px;
893 max-width: 720px;
894 }
895 .v2-steps li {
896 counter-increment: step;
897 position: relative;
898 padding-left: 44px;
899 }
900 .v2-steps li::before {
901 content: counter(step);
902 position: absolute;
903 left: 0;
904 top: 0;
905 width: 28px;
906 height: 28px;
907 border-radius: 50%;
908 border: 1px solid var(--alc-hairline-3);
909 color: var(--alc-fg-1);
910 font-family: var(--alc-font-mono);
911 font-size: 13px;
912 display: flex;
913 align-items: center;
914 justify-content: center;
915 }
916 .v2-steps__label {
917 display: block;
918 font-weight: 600;
919 color: var(--alc-fg-1);
920 margin-bottom: 8px;
921 }
922 .v2-steps__code {
923 display: inline-block;
924 font-family: var(--alc-font-mono);
925 font-size: 14px;
926 background: var(--alc-bg-code);
927 color: var(--alc-fg-invert);
928 padding: 10px 16px;
929 border-radius: var(--alc-radius);
930 }
931 /* One-command steps: label and command share the number's row. */
932 .v2-steps__inline {
933 display: flex;
934 align-items: center;
935 flex-wrap: wrap;
936 gap: 6px 12px;
937 min-height: 28px;
938 }
939 .v2-steps__inline .v2-steps__label {
940 margin-bottom: 0;
941 }
942 .v2-steps__inline .v2-steps__code {
943 font-size: 13px;
944 padding: 3px 10px;
945 border-radius: 6px;
946 }
947 .v2-cta-buttons {
948 display: flex;
949 gap: 12px;
950 margin-top: 40px;
951 }
952</style>