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
2323 # jetbrains setting folder
2424 .idea/
2525 .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.
5+import { setupPrompt } from "../../content/site-copy";
6+
7+const logos = import.meta.glob<string>("../../assets/logos/*.svg", {
8+ eager: true,
9+ query: "?url",
10+ import: "default",
11+});
12+const 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+};
17+const 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 @@
11 ---
22 // Document-style homepage body. Reads like a short paper about the product:
33 // 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.
56 // Sits inside the rail layout; the rail's "On this page" list points at the
67 // section ids below.
78 import { AnimatedBeamDemo } from "../animated-beam-demo";
89 import { ContextBloatDemo } from "../context-bloat-demo";
9−import { setupPrompt } from "../../content/site-copy";
1010 import Testimonials from "./testimonials.astro";
11+import IntroSteps from "./intro-steps.astro";
12+import CopyPrompt from "./copy-prompt.astro";
1113 import "../../styles/variants/doc.css";
1214 // Avatar goes through Vite so it ships as a hashed asset; new /public files
1315 // are not picked up by the deploy pipeline.
1416 import rhysAvatar from "../../assets/rhys-sullivan.jpg";
15−// Brand marks for the hover reveals in the intro steps.
16−const logos = import.meta.glob<string>("../../assets/logos/*.svg", {
17− eager: true,
18− query: "?url",
19− import: "default",
20−});
21−const 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−};
26−const toolLogos = ["github", "linear", "sentry", "stripe", "notion", "slack", "jira", "gmail"];
27−const 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.
29−const agentLogosShort = ["claude", "codex", "cursor"];
3017 const X_URL = "https://x.com/RhysSullivan";
3118
3219 interface Props {
@@ -39,91 +26,15 @@ const { gh } = Astro.props; · 6 unmodified lines
3926 {/* ── Intro ── */}
4027 <header class="doc__intro">
4128 <h1 class="doc__title">Executor is an MCP gateway.</h1>
42− <ol class="doc__steps">
43− <li>
44− <span class="doc__num">01</span>
45− <span>
46− Connect{" "}
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>
29+ <IntroSteps />
30+ <p class="doc__subhead">One MCP. All your accounts. Free for up to three people.</p>
31+ <div class="doc__actions">
32+ <a href="/cloud" class="btn-primary">Start on Cloud</a>
33+ <CopyPrompt />
12634 </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>
12738 </header>
12839
12940 <figure class="doc__fig doc__fig--hero">
@@ -305,29 +216,6 @@ const { gh } = Astro.props; · 175 unmodified lines
305216 </section>
306217 </article>
307218
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−
331219 <script>
332220 // Show one download button for the visitor's OS and tuck the rest behind
333221 // "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.
3+const logos = import.meta.glob<string>("../../assets/logos/*.svg", {
4+ eager: true,
5+ query: "?url",
6+ import: "default",
7+});
8+const 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+};
13+const toolLogos = ["github", "linear", "sentry", "stripe", "notion", "slack", "jira", "gmail"];
14+const 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 @@
11 ---
22 // Rail, variant B: "Gridline rail".
3−// Compact and technical: mark-only wordmark, a tight link stack, an outlined
4−// CTA, and a numbered mono index. A full-height hairline closes the right edge.
3+// Compact and technical: mark-only wordmark, a tight link stack, two quiet
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.
57 import "../../styles/variants/rail.css";
6−import ycBackedBy from "../../assets/yc-backed-by.svg?url";
78 import rhysAvatar from "../../assets/rhys-sullivan.jpg";
89
910 const X_URL = "https://x.com/RhysSullivan";
@@ -47,38 +48,13 @@ const pad = (index: number) => String(index + 1).padStart(2, "0"); · 37 unmodified lines
4748 <a href={SIGN_IN} class="rail__link rail__link--keep">Sign in</a>
4849 </nav>
4950
50− <div class="rail__cta">
51− <a href="#install" class="rail__box rail__box--primary">Get started</a>
52− <a
53− href={GH}
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>}
51+ <div class="rail__meta">
52+ <a href={GH} target="_blank" rel="noopener" class="rail__meta-line">
53+ <span>Open source</span>
54+ {stars != null && <span class="rail__meta-n">★ {stars}</span>}
7355 </a>
74− <a
75− href="https://www.ycombinator.com"
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" />
56+ <a href="https://www.ycombinator.com" target="_blank" rel="noopener noreferrer" class="rail__meta-line">
57+ <span>Backed by Y Combinator</span>
8258 </a>
8359 </div>
8460
apps/marketing/src/styles/variants/doc.css+41−76
@@ -299,48 +299,6 @@ · 298 unmodified lines
299299 text-decoration-color: var(--color-rule-strong);
300300 }
301301
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−}
344302 .doc__cmd {
345303 font-family: var(--font-mono);
346304 font-size: 12.5px;
@@ -737,20 +695,6 @@ a.doc__term-v:hover { · 390 unmodified lines
737695 display: none;
738696 }
739697
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−
754698 .doc__subhead {
755699 font-size: 17px;
756700 line-height: 1.5;
@@ -858,25 +802,12 @@ a.doc__term-v:hover { · 101 unmodified lines
858802 width: auto;
859803 max-width: none;
860804 }
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− }
874805 .doc__actions {
875806 flex-direction: column;
876807 align-items: stretch;
877808 }
878809 .doc__actions > .btn-primary,
879− .doc__actions > .btn-secondary,
810+ .doc__actions > .btn-ghost,
880811 .doc__actions > .doc__cmd,
881812 .doc__actions > .doc__dl {
882813 width: 100%;
@@ -936,12 +867,6 @@ a.doc__term-v:hover { · 53 unmodified lines
936867 justify-content: center;
937868 gap: 0.6rem;
938869 }
939− .doc__tabs {
940− margin-inline: auto;
941− }
942− .doc__tabpanes p {
943− margin-inline: auto;
944− }
945870 .doc__actions {
946871 justify-content: center;
947872 }
@@ -1015,3 +940,43 @@ a.doc__term-v:hover { · 67 unmodified lines
1015940 max-width: 720px;
1016941 }
1017942 }
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
133133 .rail--b .rail__link[aria-current="page"]::before {
134134 opacity: 1;
135135 }
136−.rail--b .rail__cta {
136+/* ── Quiet rail meta: star count and YC as text lines, not boxes ── */
137+.rail--b .rail__meta {
137138 display: grid;
138− gap: 8px;
139+ gap: 4px;
139140 margin-top: 16px;
140− --rail-box-h: 36px;
141+ padding-top: 12px;
142+ border-top: 1px solid var(--color-rule);
141143 }
142−/* One stack, one button language: same height, width, radius, hairline
143− border, and sans label. The primary is filled ink; the others are quiet. */
144−.rail--b .rail__box {
144+.rail--b .rail__meta-line {
145145 display: flex;
146146 align-items: center;
147− justify-content: center;
148− gap: 7px;
149− height: var(--rail-box-h);
150− width: 100%;
151− box-sizing: border-box;
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;
147+ justify-content: space-between;
148+ gap: 8px;
149+ font-size: 12.5px;
150+ color: var(--color-ink-3);
151+ line-height: 1.6;
176152 }
177−/* Star pill: same box, sans label, count in mono like the app's metadata. */
178−.rail--b .rail__box.gh-star {
153+.rail--b .rail__meta-line:hover {
179154 color: var(--color-ink);
180155 }
181−.rail--b .rail__box.gh-star .gh-star__ico {
182− width: 12px;
183− height: 12px;
184−}
185−.rail--b .rail__box.gh-star .gh-star__n {
156+.rail--b .rail__meta-n {
186157 font-family: var(--font-mono);
187− font-weight: 400;
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;
158+ font-size: 11.5px;
204159 }
160+
205161 .rail--b .rail__toc {
206162 gap: 8px;
207163 }
@@ -314,8 +270,7 @@ · 106 unmodified lines
314270 .rail .rail__foot,
315271 .rail .rail__contact,
316272 .rail .rail__secondary,
317− .rail .gh-star,
318− .rail .rail__box--yc {
273+ .rail .rail__meta {
319274 display: none;
320275 }
321276 .rail .rail__stack {
@@ -337,16 +292,6 @@ · 15 unmodified lines
337292 .rail .rail__link::before {
338293 display: none;
339294 }
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− }
350295 }
351296 @media (max-width: 479.98px) {
352297 .rail .rail__nav .rail__link:not(.rail__link--keep) {