History

marketing: one door on the homepage hero (#2040)

Drop the Cloud / Desktop / Self-hosted tabs from the hero. One Start on Cloud button, a ghost Copy setup prompt beside it, and a fine-print link to the Run it section for the local options. The rail stops selling: the Get started, star, and YC boxes become two quiet text lines. Extract the intro steps and the copy button into components.

Rhys Sullivan committed 1 month ago

Commit 67db4f61 parent 9b718a57 files · +170 −305Browse files

Unified · Split

7 files
  1. apps/marketing/.gitignore +1 −0
  2. apps/marketing/src/components/variants/copy-prompt.astro +36 −0
  3. apps/marketing/src/components/variants/doc-body.astro +12 −124
  4. apps/marketing/src/components/variants/intro-steps.astro +54 −0
  5. apps/marketing/src/components/variants/rail-b.astro +10 −34
  6. apps/marketing/src/styles/variants/doc.css +41 −76
  7. apps/marketing/src/styles/variants/rail.css +16 −71
apps/marketing/.gitignore+1−0
@@ -23,3 +23,4 @@ pnpm-debug.log* · 22 unmodified lines
23# jetbrains setting folder23# jetbrains setting folder
24.idea/24.idea/
25.vercel25.vercel
26.preview/
apps/marketing/src/components/variants/copy-prompt.astro+36−0
@@ -0,0 +1,36 @@
1---
2// The "Copy setup prompt" button: a dashed ghost button beside the primary,
3// carrying the three agent marks. Copies `setupPrompt`; the click handler
4// lives in pages/index.astro.
5import { setupPrompt } from "../../content/site-copy";
6
7const logos = import.meta.glob<string>("../../assets/logos/*.svg", {
8 eager: true,
9 query: "?url",
10 import: "default",
11});
12const logo = (name: string): string => {
13 const hit = Object.entries(logos).find(([k]) => k.endsWith(`/${name}.svg`));
14 if (!hit) throw new Error(`missing logo ${name}`);
15 return hit[1];
16};
17const marks = ["claude", "codex", "cursor"];
18
19
20---
21
22<button
23 type="button"
24 class="btn-ghost copy-btn doc__copy"
25 data-copy={setupPrompt}
26 data-event="marketing_setup_prompt_copied"
27>
28 <span class="doc__copy-logos" aria-hidden="true">
29 {marks.map((n) => <img src={logo(n)} alt="" width="14" height="14" />)}
30 </span>
31 <span>Copy setup prompt</span>
32 <span class="copy-icons" aria-hidden="true">
33 <svg class="copy-icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>
34 <svg class="copy-icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>
35 </span>
36</button>
apps/marketing/src/components/variants/doc-body.astro+12−124
@@ -1,32 +1,19 @@
1---1---
2// Document-style homepage body. Reads like a short paper about the product:2// Document-style homepage body. Reads like a short paper about the product:
3// one modest headline, plain first-person prose, six beats separated by3// one modest headline, plain first-person prose, six beats separated by
4// hairlines, the two demos kept as numbered figures, and one way to start.4// hairlines, the two demos kept as numbered figures, and one door in: Cloud,
5// with a setup prompt for visitors who already have an agent open.
5// Sits inside the rail layout; the rail's "On this page" list points at the6// Sits inside the rail layout; the rail's "On this page" list points at the
6// section ids below.7// section ids below.
7import { AnimatedBeamDemo } from "../animated-beam-demo";8import { AnimatedBeamDemo } from "../animated-beam-demo";
8import { ContextBloatDemo } from "../context-bloat-demo";9import { ContextBloatDemo } from "../context-bloat-demo";
9import { setupPrompt } from "../../content/site-copy";
10import Testimonials from "./testimonials.astro";10import Testimonials from "./testimonials.astro";
11import IntroSteps from "./intro-steps.astro";
12import CopyPrompt from "./copy-prompt.astro";
11import "../../styles/variants/doc.css";13import "../../styles/variants/doc.css";
12// Avatar goes through Vite so it ships as a hashed asset; new /public files14// Avatar goes through Vite so it ships as a hashed asset; new /public files
13// are not picked up by the deploy pipeline.15// are not picked up by the deploy pipeline.
14import rhysAvatar from "../../assets/rhys-sullivan.jpg";16import rhysAvatar from "../../assets/rhys-sullivan.jpg";
15// Brand marks for the hover reveals in the intro steps.
16const logos = import.meta.glob<string>("../../assets/logos/*.svg", {
17 eager: true,
18 query: "?url",
19 import: "default",
20});
21const logo = (name: string): string => {
22 const hit = Object.entries(logos).find(([k]) => k.endsWith(`/${name}.svg`));
23 if (!hit) throw new Error(`missing logo ${name}`);
24 return hit[1];
25};
26const toolLogos = ["github", "linear", "sentry", "stripe", "notion", "slack", "jira", "gmail"];
27const agentLogos = ["claude", "codex", "cursor", "chatgpt", "gemini", "hermes", "copilot", "windsurf", "opencode", "zed", "grok"];
28// The three marks on the copy button; the full list would crowd it.
29const agentLogosShort = ["claude", "codex", "cursor"];
30const X_URL = "https://x.com/RhysSullivan";17const X_URL = "https://x.com/RhysSullivan";
31 18
32interface Props {19interface Props {
@@ -39,91 +26,15 @@ const { gh } = Astro.props; · 6 unmodified lines
39 {/* ── Intro ── */}26 {/* ── Intro ── */}
40 <header class="doc__intro">27 <header class="doc__intro">
41 <h1 class="doc__title">Executor is an MCP gateway.</h1>28 <h1 class="doc__title">Executor is an MCP gateway.</h1>
42 <ol class="doc__steps">29 <IntroSteps />
43 <li>30 <p class="doc__subhead">One MCP. All your accounts. Free for up to three people.</p>
44 <span class="doc__num">01</span>31 <div class="doc__actions">
45 <span>32 <a href="/cloud" class="btn-primary">Start on Cloud</a>
46 Connect{" "}33 <CopyPrompt />
47 <span class="doc__pop" tabindex="0">
48 <span class="doc__pop-word">everything</span>
49 <span class="doc__pop-panel" aria-hidden="true" inert>
50 {toolLogos.map((n, i) => (
51 <span class="doc__pop-tile" style={`--i:${i}`}><img src={logo(n)} alt="" width="18" height="18" /></span>
52 ))}
53 <span class="doc__pop-pill" style={`--i:${toolLogos.length}`}>any OpenAPI, GraphQL, or MCP</span>
54 </span>
55 </span>
56 {" "}to Executor.
57 </span>
58 </li>
59 <li>
60 <span class="doc__num">02</span>
61 <span>
62 Give{" "}
63 <span class="doc__pop" tabindex="0">
64 <span class="doc__pop-word">your agent</span>
65 <span class="doc__pop-panel" aria-hidden="true" inert>
66 {agentLogos.map((n, i) => (
67 <span class="doc__pop-tile" style={`--i:${i}`}><img src={logo(n)} alt="" width="18" height="18" /></span>
68 ))}
69 <span class="doc__pop-pill" style={`--i:${agentLogos.length}`}>any MCP client</span>
70 </span>
71 </span>
72 {" "}the Executor tool.
73 </span>
74 </li>
75 <li>
76 <span class="doc__num">03</span>
77 <span>That's it.{" "}<a href="#install">Get started.</a></span>
78 </li>
79 </ol>
80 <p class="doc__subhead">One MCP. All your accounts.</p>
81 <div class="doc__tabs" role="tablist" aria-label="How to run Executor">
82 <button type="button" role="tab" aria-selected="true" data-doc-tab="cloud">Cloud</button>
83 <button type="button" role="tab" aria-selected="false" data-doc-tab="local">Desktop / CLI</button>
84 <button type="button" role="tab" aria-selected="false" data-doc-tab="self">Self-hosted</button>
85 </div>
86 <div class="doc__tabpanes">
87 <div role="tabpanel" data-doc-pane="cloud">
88 <p>Sign in, point your agent at one URL, done. Free for up to three people and 100,000 executions a month.</p>
89 <div class="doc__actions"><a href="/cloud" class="btn-primary">Start on Cloud</a><button type="button" class="btn-secondary copy-btn doc__copy" data-copy={setupPrompt} data-event="marketing_setup_prompt_copied">
90 <span class="doc__copy-logos" aria-hidden="true">
91 {agentLogosShort.map((n) => <img src={logo(n)} alt="" width="14" height="14" />)}
92 </span>
93 <span>Copy setup prompt</span>
94 <span class="copy-icons" aria-hidden="true">
95 <svg class="copy-icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>
96 <svg class="copy-icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>
97 </span>
98 </button></div>
99 </div>
100 <div role="tabpanel" data-doc-pane="local" data-inactive aria-hidden="true" inert>
101 <p>Nothing leaves your machine. A desktop app for Mac, Windows, and Linux, or a CLI that runs in the background. Same local endpoint either way.</p>
102 <div class="doc__actions"><span class="doc__dl" data-download>
103 <a data-os="mac-arm64" href={`${gh}/releases/latest/download/executor-desktop-mac-arm64.dmg`} class="btn-primary">Download for Mac</a>
104 <a data-os="mac-x64" href={`${gh}/releases/latest/download/executor-desktop-mac-x64.dmg`} class="btn-primary">Download for Mac (Intel)</a>
105 <a data-os="win-x64" href={`${gh}/releases/latest/download/executor-desktop-win-x64.exe`} class="btn-primary">Download for Windows</a>
106 <a data-os="linux-x64" href={`${gh}/releases/latest/download/executor-desktop-linux-x86_64.AppImage`} class="btn-primary">Download for Linux</a>
107 <details class="download-other doc__dl-other hidden" data-download-other>
108 <summary>Other platforms ▾</summary>
109 <div class="download-other-list" data-download-other-list></div>
110 </details>
111 </span><code class="doc__cmd">npm i -g executor</code><button type="button" class="btn-secondary copy-btn doc__copy" data-copy={setupPrompt} data-event="marketing_setup_prompt_copied">
112 <span class="doc__copy-logos" aria-hidden="true">
113 {agentLogosShort.map((n) => <img src={logo(n)} alt="" width="14" height="14" />)}
114 </span>
115 <span>Copy setup prompt</span>
116 <span class="copy-icons" aria-hidden="true">
117 <svg class="copy-icon-copy" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>
118 <svg class="copy-icon-check" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"></path></svg>
119 </span>
120 </button></div>
121 </div>
122 <div role="tabpanel" data-doc-pane="self" data-inactive aria-hidden="true" inert>
123 <p>One Docker image on your own box. It is the same code Cloud runs.</p>
124 <div class="doc__actions"><a href="https://executor.sh/docs/hosted/docker" class="btn-primary">Self-hosting docs</a><a href={gh} class="btn-secondary">Source on GitHub</a></div>
125 </div>
126 </div>34 </div>
35 <p class="doc__subhead doc__subhead--fine">
36 Rather keep it on your machine? <a href="#install">Desktop, CLI, or self-hosted ↓</a>
37 </p>
127 </header>38 </header>
128 39
129 <figure class="doc__fig doc__fig--hero">40 <figure class="doc__fig doc__fig--hero">
@@ -305,29 +216,6 @@ const { gh } = Astro.props; · 175 unmodified lines
305 </section>216 </section>
306</article>217</article>
307 218
308<script>
309 for (const list of document.querySelectorAll<HTMLElement>(".doc__tabs")) {
310 const root = list.parentElement!;
311 list.addEventListener("click", (e) => {
312 const tab = (e.target as HTMLElement).closest<HTMLElement>("[data-doc-tab]");
313 if (!tab) return;
314 const key = tab.dataset.docTab;
315 for (const t of list.querySelectorAll<HTMLElement>("[data-doc-tab]")) {
316 t.setAttribute("aria-selected", String(t === tab));
317 }
318 for (const p of root.querySelectorAll<HTMLElement>("[data-doc-pane]")) {
319 const off = p.dataset.docPane !== key;
320 p.toggleAttribute("data-inactive", off);
321 p.toggleAttribute("inert", off);
322 p.setAttribute("aria-hidden", String(off));
323 for (const el of p.querySelectorAll<HTMLElement>("a, button")) {
324 el.tabIndex = off ? -1 : 0;
325 }
326 }
327 });
328 }
329</script>
330
331<script>219<script>
332 // Show one download button for the visitor's OS and tuck the rest behind220 // Show one download button for the visitor's OS and tuck the rest behind
333 // "Other platforms". Without JS or on an unknown OS, all four stay visible.221 // "Other platforms". Without JS or on an unknown OS, all four stay visible.
apps/marketing/src/components/variants/intro-steps.astro+54−0
@@ -0,0 +1,54 @@
1---
2// The three numbered intro steps with the hover-reveal logo panels.
3const logos = import.meta.glob<string>("../../assets/logos/*.svg", {
4 eager: true,
5 query: "?url",
6 import: "default",
7});
8const logo = (name: string): string => {
9 const hit = Object.entries(logos).find(([k]) => k.endsWith(`/${name}.svg`));
10 if (!hit) throw new Error(`missing logo ${name}`);
11 return hit[1];
12};
13const toolLogos = ["github", "linear", "sentry", "stripe", "notion", "slack", "jira", "gmail"];
14const agentLogos = ["claude", "codex", "cursor", "chatgpt", "gemini", "hermes", "copilot", "windsurf", "opencode", "zed", "grok"];
15---
16
17<ol class="doc__steps">
18 <li>
19 <span class="doc__num">01</span>
20 <span>
21 Connect{" "}
22 <span class="doc__pop" tabindex="0">
23 <span class="doc__pop-word">everything</span>
24 <span class="doc__pop-panel" aria-hidden="true" inert>
25 {toolLogos.map((n, i) => (
26 <span class="doc__pop-tile" style={`--i:${i}`}><img src={logo(n)} alt="" width="18" height="18" /></span>
27 ))}
28 <span class="doc__pop-pill" style={`--i:${toolLogos.length}`}>any OpenAPI, GraphQL, or MCP</span>
29 </span>
30 </span>
31 {" "}to Executor.
32 </span>
33 </li>
34 <li>
35 <span class="doc__num">02</span>
36 <span>
37 Give{" "}
38 <span class="doc__pop" tabindex="0">
39 <span class="doc__pop-word">your agent</span>
40 <span class="doc__pop-panel" aria-hidden="true" inert>
41 {agentLogos.map((n, i) => (
42 <span class="doc__pop-tile" style={`--i:${i}`}><img src={logo(n)} alt="" width="18" height="18" /></span>
43 ))}
44 <span class="doc__pop-pill" style={`--i:${agentLogos.length}`}>any MCP client</span>
45 </span>
46 </span>
47 {" "}the Executor tool.
48 </span>
49 </li>
50 <li>
51 <span class="doc__num">03</span>
52 <span>That's it.</span>
53 </li>
54</ol>
apps/marketing/src/components/variants/rail-b.astro+10−34
@@ -1,9 +1,10 @@
1---1---
2// Rail, variant B: "Gridline rail".2// Rail, variant B: "Gridline rail".
3// Compact and technical: mark-only wordmark, a tight link stack, an outlined3// Compact and technical: mark-only wordmark, a tight link stack, two quiet
4// CTA, and a numbered mono index. A full-height hairline closes the right edge.4// meta lines (open source, YC), and a numbered mono index. The rail does not
5// sell; the hero owns the call to action. A full-height hairline closes the
6// right edge.
5import "../../styles/variants/rail.css";7import "../../styles/variants/rail.css";
6import ycBackedBy from "../../assets/yc-backed-by.svg?url";
7import rhysAvatar from "../../assets/rhys-sullivan.jpg";8import rhysAvatar from "../../assets/rhys-sullivan.jpg";
8 9
9const X_URL = "https://x.com/RhysSullivan";10const X_URL = "https://x.com/RhysSullivan";
@@ -47,38 +48,13 @@ const pad = (index: number) => String(index + 1).padStart(2, "0"); · 37 unmodified lines
47 <a href={SIGN_IN} class="rail__link rail__link--keep">Sign in</a>48 <a href={SIGN_IN} class="rail__link rail__link--keep">Sign in</a>
48 </nav>49 </nav>
49 50
50 <div class="rail__cta">51 <div class="rail__meta">
51 <a href="#install" class="rail__box rail__box--primary">Get started</a>52 <a href={GH} target="_blank" rel="noopener" class="rail__meta-line">
52 <a53 <span>Open source</span>
53 href={GH}54 {stars != null && <span class="rail__meta-n">★ {stars}</span>}
54 target="_blank"
55 rel="noopener"
56 class="rail__box gh-star"
57 aria-label="Star Executor on GitHub"
58 >
59 <svg
60 class="gh-star__ico"
61 width="13"
62 height="13"
63 viewBox="0 0 16 16"
64 aria-hidden="true"
65 >
66 <path
67 fill="#e3b341"
68 d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25z"
69 ></path>
70 </svg>
71 <span>Star on GitHub</span>
72 {stars != null && <span class="gh-star__n">{stars}</span>}
73 </a>55 </a>
74 <a56 <a href="https://www.ycombinator.com" target="_blank" rel="noopener noreferrer" class="rail__meta-line">
75 href="https://www.ycombinator.com"57 <span>Backed by Y Combinator</span>
76 target="_blank"
77 rel="noopener noreferrer"
78 class="rail__box rail__box--yc"
79 aria-label="Backed by Y Combinator"
80 >
81 <img src={ycBackedBy} alt="Backed by Y Combinator" width="350" height="125" />
82 </a>58 </a>
83 </div>59 </div>
84 60
apps/marketing/src/styles/variants/doc.css+41−76
@@ -299,48 +299,6 @@ · 298 unmodified lines
299 text-decoration-color: var(--color-rule-strong);299 text-decoration-color: var(--color-rule-strong);
300}300}
301 301
302.doc__tabs {
303 display: inline-flex;
304 gap: 0;
305 border: 1px solid var(--color-rule);
306 border-radius: 8px;
307 padding: 3px;
308 margin-bottom: 1rem;
309 background: var(--color-surface-2);
310}
311.doc__tabs button {
312 font: inherit;
313 font-size: 13.5px;
314 font-weight: 500;
315 padding: 0.4rem 0.85rem;
316 border: 0;
317 border-radius: 6px;
318 background: transparent;
319 color: var(--color-ink-2);
320 cursor: pointer;
321}
322.doc__tabs button[aria-selected="true"] {
323 background: var(--color-surface);
324 color: var(--color-ink);
325 box-shadow:
326 0 1px 2px rgba(0, 0, 0, 0.06),
327 0 0 0 1px var(--color-rule);
328}
329.doc__tabpanes p {
330 font-size: 15.5px;
331 line-height: 1.6;
332 color: var(--color-ink-2);
333 margin-bottom: 1rem;
334 max-width: 60ch;
335 /* Always two lines tall, so the buttons sit at the same height on every tab. */
336 min-height: calc(2 * 1.6em);
337}
338.doc__tabpanes .doc__actions {
339 min-height: 46px;
340}
341.doc__tabpanes .doc__actions {
342 margin-bottom: 1.1rem;
343}
344.doc__cmd {302.doc__cmd {
345 font-family: var(--font-mono);303 font-family: var(--font-mono);
346 font-size: 12.5px;304 font-size: 12.5px;
@@ -737,20 +695,6 @@ a.doc__term-v:hover { · 390 unmodified lines
737 display: none;695 display: none;
738}696}
739 697
740/* Tab panes share one grid cell, so the tallest one sets the height and
741 switching tabs never shifts the content below. Hidden panes stay in the
742 layout but are invisible and inert. */
743.doc__tabpanes {
744 display: grid;
745}
746.doc__tabpanes > [role="tabpanel"] {
747 grid-area: 1 / 1;
748}
749.doc__tabpanes > [role="tabpanel"][data-inactive] {
750 visibility: hidden;
751 pointer-events: none;
752}
753
754.doc__subhead {698.doc__subhead {
755 font-size: 17px;699 font-size: 17px;
756 line-height: 1.5;700 line-height: 1.5;
@@ -858,25 +802,12 @@ a.doc__term-v:hover { · 101 unmodified lines
858 width: auto;802 width: auto;
859 max-width: none;803 max-width: none;
860 }804 }
861 .doc__tabs {
862 display: flex;
863 width: 100%;
864 }
865 .doc__tabs button {
866 flex: 1;
867 padding: 0.45rem 0.4rem;
868 font-size: 13px;
869 white-space: nowrap;
870 }
871 .doc__tabpanes p {
872 min-height: 0;
873 }
874 .doc__actions {805 .doc__actions {
875 flex-direction: column;806 flex-direction: column;
876 align-items: stretch;807 align-items: stretch;
877 }808 }
878 .doc__actions > .btn-primary,809 .doc__actions > .btn-primary,
879 .doc__actions > .btn-secondary,810 .doc__actions > .btn-ghost,
880 .doc__actions > .doc__cmd,811 .doc__actions > .doc__cmd,
881 .doc__actions > .doc__dl {812 .doc__actions > .doc__dl {
882 width: 100%;813 width: 100%;
@@ -936,12 +867,6 @@ a.doc__term-v:hover { · 53 unmodified lines
936 justify-content: center;867 justify-content: center;
937 gap: 0.6rem;868 gap: 0.6rem;
938 }869 }
939 .doc__tabs {
940 margin-inline: auto;
941 }
942 .doc__tabpanes p {
943 margin-inline: auto;
944 }
945 .doc__actions {870 .doc__actions {
946 justify-content: center;871 justify-content: center;
947 }872 }
@@ -1015,3 +940,43 @@ a.doc__term-v:hover { · 67 unmodified lines
1015 max-width: 720px;940 max-width: 720px;
1016 }941 }
1017}942}
943
944/* ── Fine print under the hero buttons ── */
945.doc__subhead--fine {
946 font-size: 15px;
947 color: var(--color-ink-3);
948 max-width: 52ch;
949}
950.doc__subhead--fine a {
951 color: var(--color-ink-2);
952 text-decoration: underline;
953 text-underline-offset: 3px;
954 text-decoration-color: var(--color-rule-strong);
955}
956.doc__subhead--fine a:hover {
957 color: var(--color-ink);
958}
959
960/* ── Ghost copy button: text weight of a link, hit area of a button ── */
961.btn-ghost {
962 display: inline-flex;
963 align-items: center;
964 gap: 0.4rem;
965 background: transparent;
966 color: var(--color-ink-2);
967 border: 1px dashed var(--color-rule-strong);
968 border-radius: 8px;
969 padding: 0.7rem 1.1rem;
970 font-family: inherit;
971 font-weight: 500;
972 font-size: 14.5px;
973 letter-spacing: -0.005em;
974 cursor: pointer;
975 transition:
976 color 0.15s ease,
977 border-color 0.15s ease;
978}
979.btn-ghost:hover {
980 color: var(--color-ink);
981 border-color: var(--color-ink-3);
982}
apps/marketing/src/styles/variants/rail.css+16−71
@@ -133,75 +133,31 @@ · 132 unmodified lines
133.rail--b .rail__link[aria-current="page"]::before {133.rail--b .rail__link[aria-current="page"]::before {
134 opacity: 1;134 opacity: 1;
135}135}
136.rail--b .rail__cta {136/* ── Quiet rail meta: star count and YC as text lines, not boxes ── */
137.rail--b .rail__meta {
137 display: grid;138 display: grid;
138 gap: 8px;139 gap: 4px;
139 margin-top: 16px;140 margin-top: 16px;
140 --rail-box-h: 36px;141 padding-top: 12px;
142 border-top: 1px solid var(--color-rule);
141}143}
142/* One stack, one button language: same height, width, radius, hairline144.rail--b .rail__meta-line {
143 border, and sans label. The primary is filled ink; the others are quiet. */
144.rail--b .rail__box {
145 display: flex;145 display: flex;
146 align-items: center;146 align-items: center;
147 justify-content: center;147 justify-content: space-between;
148 gap: 7px;148 gap: 8px;
149 height: var(--rail-box-h);149 font-size: 12.5px;
150 width: 100%;150 color: var(--color-ink-3);
151 box-sizing: border-box;151 line-height: 1.6;
152 padding: 0 12px;
153 border: 1px solid var(--color-rule-strong);
154 border-radius: 6px;
155 background: var(--color-surface);
156 font-family: var(--font-sans);
157 font-size: 13px;
158 font-weight: 500;
159 color: var(--color-ink);
160 overflow: hidden;
161 transition:
162 background 0.15s ease,
163 color 0.15s ease,
164 border-color 0.15s ease;
165}
166.rail--b .rail__box:hover {
167 border-color: var(--color-ink);
168}
169.rail--b .rail__box--primary {
170 background: var(--color-ink);
171 border-color: var(--color-ink);
172 color: var(--color-surface);
173}
174.rail--b .rail__box--primary:hover {
175 background: #1f1f24;
176}152}
177/* Star pill: same box, sans label, count in mono like the app's metadata. */153.rail--b .rail__meta-line:hover {
178.rail--b .rail__box.gh-star {
179 color: var(--color-ink);154 color: var(--color-ink);
180}155}
181.rail--b .rail__box.gh-star .gh-star__ico {156.rail--b .rail__meta-n {
182 width: 12px;
183 height: 12px;
184}
185.rail--b .rail__box.gh-star .gh-star__n {
186 font-family: var(--font-mono);157 font-family: var(--font-mono);
187 font-weight: 400;158 font-size: 11.5px;
188 font-size: 12px;
189 color: var(--color-ink-3);
190 border-left: 1px solid var(--color-rule);
191 padding-left: 8px;
192 margin-left: 1px;
193}
194/* The YC badge is a rounded cream card at 350x125; letting it fill the box
195 edge to edge keeps the badge's own corners hidden behind ours. */
196.rail--b .rail__box--yc {
197 padding: 0;
198 background: #f6f6ef;
199}
200.rail--b .rail__box--yc img {
201 height: 100%;
202 width: auto;
203 display: block;
204}159}
160
205.rail--b .rail__toc {161.rail--b .rail__toc {
206 gap: 8px;162 gap: 8px;
207}163}
@@ -314,8 +270,7 @@ · 106 unmodified lines
314 .rail .rail__foot,270 .rail .rail__foot,
315 .rail .rail__contact,271 .rail .rail__contact,
316 .rail .rail__secondary,272 .rail .rail__secondary,
317 .rail .gh-star,273 .rail .rail__meta {
318 .rail .rail__box--yc {
319 display: none;274 display: none;
320 }275 }
321 .rail .rail__stack {276 .rail .rail__stack {
@@ -337,16 +292,6 @@ · 15 unmodified lines
337 .rail .rail__link::before {292 .rail .rail__link::before {
338 display: none;293 display: none;
339 }294 }
340 .rail .rail__cta {
341 display: block;
342 margin-top: 0;
343 }
344 .rail--b .rail__box--primary {
345 width: auto;
346 height: 32px;
347 padding: 0 12px;
348 font-size: 13px;
349 }
350}295}
351@media (max-width: 479.98px) {296@media (max-width: 479.98px) {
352 .rail .rail__nav .rail__link:not(.rail__link--keep) {297 .rail .rail__nav .rail__link:not(.rail__link--keep) {