From bf31ec25e0a62c670591ab4755d00ca8d633042c Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 14:06:09 -0400 Subject: [PATCH 1/8] feat: add pricing page (support + services model) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a public pricing page monetizing human expertise around the self-hosted, Apache 2.0 product — no license fees, no seat fees, and no percentage of agent spend. - Support subscriptions: Community (free), Production ($1,500/mo, introductory), Enterprise (from $6,000/mo). Enterprise SLA framed as custom/optional-24x7 rather than an unconditional public promise, plus a Production support-boundary note. - Fixed-scope services: Readiness Review, Integration Sprint, Compliance Evidence Package (worded as readiness, not legal advice), Custom work, and Team Enablement Workshop. - Lead differentiator: support pricing is flat and independent of token, request, agent, tenant, or downstream provider spend. - Managed-cloud waitlist teaser routing to the existing Contact form. New: pricing.md, .vitepress/theme/Pricing.vue (tier + service cards). Wires Pricing into nav (config.ts), footer (SiteFooter.vue), and the global component registry (theme/index.ts). --- .vitepress/config.ts | 1 + .vitepress/theme/Pricing.vue | 370 ++++++++++++++++++++++++++++++++ .vitepress/theme/SiteFooter.vue | 1 + .vitepress/theme/index.ts | 2 + pricing.md | 55 +++++ 5 files changed, 429 insertions(+) create mode 100644 .vitepress/theme/Pricing.vue create mode 100644 pricing.md diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 6d731e73..e89f7866 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -273,6 +273,7 @@ export default defineConfig({ ], }, { text: 'Blog', link: '/blog/' }, + { text: 'Pricing', link: '/pricing' }, { text: 'Partners', link: '/design-partners' }, { text: 'Contact', link: '/contact' }, { text: 'GitHub', link: 'https://github.com/runcycles' } diff --git a/.vitepress/theme/Pricing.vue b/.vitepress/theme/Pricing.vue new file mode 100644 index 00000000..01dafdc0 --- /dev/null +++ b/.vitepress/theme/Pricing.vue @@ -0,0 +1,370 @@ + + + + + diff --git a/.vitepress/theme/SiteFooter.vue b/.vitepress/theme/SiteFooter.vue index 855e7db0..20a3c9ba 100644 --- a/.vitepress/theme/SiteFooter.vue +++ b/.vitepress/theme/SiteFooter.vue @@ -4,6 +4,7 @@ const links = [ { text: 'Security', href: '/security' }, { text: 'Tools', href: '/calculators/' }, { text: 'Blog', href: '/blog/' }, + { text: 'Pricing', href: '/pricing' }, { text: 'Changelog', href: '/changelog' }, { text: 'Contact', href: '/contact' }, { text: 'GitHub', href: 'https://github.com/runcycles', external: true }, diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index 483ff541..bc1229ee 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -24,6 +24,7 @@ import NetworkZones from './NetworkZones.vue' import AdoptionLadder from './AdoptionLadder.vue' import CostCalculator from './CostCalculator.vue' import BlastRadiusCalculator from './BlastRadiusCalculator.vue' +import Pricing from './Pricing.vue' export default { extends: DefaultTheme, @@ -47,6 +48,7 @@ export default { app.component('AdoptionLadder', AdoptionLadder) app.component('CostCalculator', CostCalculator) app.component('BlastRadiusCalculator', BlastRadiusCalculator) + app.component('Pricing', Pricing) }, setup() { const route = useRoute() diff --git a/pricing.md b/pricing.md new file mode 100644 index 00000000..ab3f65dd --- /dev/null +++ b/pricing.md @@ -0,0 +1,55 @@ +--- +title: "Pricing" +description: "Cycles is Apache 2.0 open-source software — self-host it for free. Paid plans add support, implementation help, and compliance-readiness assurance. No license fees, no seat fees, no percentage of agent spend." +--- + +# Pricing + +**Self-host Cycles for free. Pay only when you want our people involved.** No license key, no seat fees, and no percentage of agent spend. + +Today, paid plans support self-hosted Cycles deployments. A managed cloud (RunCycles.io) is planned but [not yet available](#managed-cloud-coming). + +## How pricing works + +- **Apache 2.0 open-source software.** Cycles is Apache 2.0 open-source software. Self-host it for free. Paid plans do not unlock hidden features; they add support, implementation help, and assurance. No license key, no feature gating, no seat fees. +- **No agent-spend tax.** We never take a percentage of your model, tool, or agent spend. Support pricing is flat and independent of tokens, requests, agents, tenants, or downstream provider spend — the opposite of gateways that bill a cut of every call. +- **You pay for humans and guarantees, not bits.** Response targets, expert integration help, and compliance-readiness evidence are the product. The software stays free. + + + +## Design partners are a separate track + +The [Design Partner program](/design-partners) is not a pricing tier. It's a small, selective, **free** cohort: founder access and priority support during a 60-day integration window, in exchange for running a real workload, pushing us hard on what's missing, and an opt-in public artifact. Paid support is for teams that want guaranteed response and expert help **without** the design-partner commitments — or who are outside or past the cohort. + +Running a real multi-tenant workload and willing to shape the next two protocol releases? [Apply to become a design partner →](/design-partners) + +## Managed cloud — coming + +A fully managed Cycles (RunCycles.io) is on the roadmap: we run Redis, the runtime, the admin server, and the events service, on a SOC 2 track, so you don't have to. It is **planned but not yet available** — until then, every paid plan above supports your self-hosted deployment. + +Join the managed-cloud waitlist → + +## Frequently asked + +**Is the software really free?** +Yes — Apache 2.0, no feature gating, no license key. Paid plans add support and assurance, not capabilities. + +**Do you take a cut of our agent spend?** +No. We never take a percentage of your model, tool, or agent spend. Support pricing is flat and independent of usage. + +**Do I need a support plan to run in production?** +No. Many teams run self-hosted on community support. Paid plans are for guaranteed response and expert help. Note that Production support covers the Cycles deployment — not building or operating your agent application. + +**How is a support plan different from the design partner program?** +The [design partner program](/design-partners) is free, selective, and feedback-for-access: you run a real workload and shape the roadmap. A support plan is open to anyone, gives you a defined response target, and carries no feedback or co-marketing obligation. + +**Can a services engagement turn into a subscription?** +Yes. A Production Readiness Review or an Integration Sprint commonly precedes a Production support plan. + +**What about managed cloud?** +It's on the roadmap. [Join the waitlist](#managed-cloud-coming). + +**How do you bill?** +Subscriptions are billed annually. Services are 50% on start and 50% on delivery, or net-30. + + From 07d35f5732bdc9085df67bd7b8ab6c5a60f9cafd Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 14:15:33 -0400 Subject: [PATCH 2/8] fix: register HomeContact globally and fix managed-cloud anchor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review findings on the pricing page: - pricing.md used but the component was only imported locally by Layout.vue, not globally registered, so it rendered nothing on the markdown page. Register it in enhanceApp. - The hero and FAQ links to #managed-cloud-coming missed: the default VitePress slugifier keeps the em dash, generating id "managed-cloud-—-coming". Add an explicit {#managed-cloud-coming} anchor so the heading display stays unchanged and the links resolve. Verified with npm run build: anchor id is now managed-cloud-coming and HomeContact renders in dist/pricing.html. --- .vitepress/theme/index.ts | 2 ++ pricing.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.vitepress/theme/index.ts b/.vitepress/theme/index.ts index bc1229ee..e8280728 100644 --- a/.vitepress/theme/index.ts +++ b/.vitepress/theme/index.ts @@ -25,6 +25,7 @@ import AdoptionLadder from './AdoptionLadder.vue' import CostCalculator from './CostCalculator.vue' import BlastRadiusCalculator from './BlastRadiusCalculator.vue' import Pricing from './Pricing.vue' +import HomeContact from './HomeContact.vue' export default { extends: DefaultTheme, @@ -49,6 +50,7 @@ export default { app.component('CostCalculator', CostCalculator) app.component('BlastRadiusCalculator', BlastRadiusCalculator) app.component('Pricing', Pricing) + app.component('HomeContact', HomeContact) }, setup() { const route = useRoute() diff --git a/pricing.md b/pricing.md index ab3f65dd..93ff5c25 100644 --- a/pricing.md +++ b/pricing.md @@ -23,7 +23,7 @@ The [Design Partner program](/design-partners) is not a pricing tier. It's a sma Running a real multi-tenant workload and willing to shape the next two protocol releases? [Apply to become a design partner →](/design-partners) -## Managed cloud — coming +## Managed cloud — coming {#managed-cloud-coming} A fully managed Cycles (RunCycles.io) is on the roadmap: we run Redis, the runtime, the admin server, and the events service, on a SOC 2 track, so you don't have to. It is **planned but not yet available** — until then, every paid plan above supports your self-hosted deployment. From d4930cf0e3f789484dc9e0eb4dc3097f343c5984 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 14:26:07 -0400 Subject: [PATCH 3/8] fix: align pricing card rows so titles, prices, and CTAs line up The Production tier carried an inline "Introductory pricing" badge the other tiers lacked, and the featured card used a 1px-wider border, so the price row sat at a different height in each card and nothing lined up. - Move the badge into its own reserved slot (fixed min-height) rendered on every tier, so all three price rows share the same baseline. - Equalize borders at 2px on all cards (featured differs only by color), removing the 1px content shift. - Give the tier price slot and the for-who line fixed heights so the description and CTA rows align across tiers regardless of cadence wrap. - Give service-card names a fixed-height slot so service prices align. - Drop the reserved heights below 768px where cards stack single-column. --- .vitepress/theme/Pricing.vue | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/.vitepress/theme/Pricing.vue b/.vitepress/theme/Pricing.vue index 01dafdc0..c6c82d99 100644 --- a/.vitepress/theme/Pricing.vue +++ b/.vitepress/theme/Pricing.vue @@ -105,6 +105,8 @@ const services = [ Most popular

{{ tier.name }}

+
+
{{ tier.badge }}
@@ -169,7 +171,7 @@ const services = [ display: flex; flex-direction: column; padding: 22px 20px; - border: 1px solid var(--vp-c-divider); + border: 2px solid var(--vp-c-divider); border-radius: 12px; background: var(--vp-c-bg-soft); transition: border-color 0.2s, transform 0.1s; @@ -181,7 +183,6 @@ const services = [ .tier.featured { border-color: var(--vp-c-brand-1); - border-width: 2px; box-shadow: 0 4px 24px -12px var(--vp-c-brand-1); } @@ -200,11 +201,14 @@ const services = [ } .tier-head { + margin-bottom: 4px; +} + +.badge-slot { display: flex; align-items: center; - gap: 8px; - flex-wrap: wrap; - margin-bottom: 10px; + min-height: 22px; + margin-bottom: 8px; } .tier-name { @@ -235,6 +239,13 @@ const services = [ margin-bottom: 12px; } +/* Fixed-height price slot so the description and CTA rows share a baseline + across all three tiers regardless of how the cadence text wraps. */ +.tier .price-row { + min-height: 72px; + align-content: flex-start; +} + .price { font-size: 30px; font-weight: 800; @@ -259,6 +270,7 @@ const services = [ color: var(--vp-c-text-2); margin: 0 0 14px; line-height: 1.5; + min-height: 42px; } .features { @@ -355,6 +367,14 @@ const services = [ border-top: none; padding-top: 0; letter-spacing: -0.01em; + min-height: 44px; +} + +@media (max-width: 767px) { + /* Single-column: no neighbor to align with, so drop the reserved slots. */ + .service-name { min-height: 0; } + .tier .price-row { min-height: 0; } + .for-who { min-height: 0; } } .service-detail { From a998e2addeee40eb9845d7a52fdfb6bc971f0bf4 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 14:29:33 -0400 Subject: [PATCH 4/8] fix: stack price above cadence on its own line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the price row to a vertical flex layout so the price (e.g. "From $6,000") always sits on its own line and the cadence ("/mo, billed annually …") wraps beneath it, instead of running inline. Applies to both tier and service cards. --- .vitepress/theme/Pricing.vue | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.vitepress/theme/Pricing.vue b/.vitepress/theme/Pricing.vue index c6c82d99..3486efa5 100644 --- a/.vitepress/theme/Pricing.vue +++ b/.vitepress/theme/Pricing.vue @@ -233,9 +233,8 @@ const services = [ .price-row { display: flex; - align-items: baseline; - flex-wrap: wrap; - gap: 6px; + flex-direction: column; + gap: 5px; margin-bottom: 12px; } @@ -243,7 +242,7 @@ const services = [ across all three tiers regardless of how the cadence text wraps. */ .tier .price-row { min-height: 72px; - align-content: flex-start; + justify-content: flex-start; } .price { From 307d211ad8c1e61e9e654fde1558563a8fadc00c Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 14:33:37 -0400 Subject: [PATCH 5/8] fix: annual total on its own line; drop Custom Integration service - Split the tier cadence into a "/mo, billed annually" line and a separate annual-total line ($18,000/yr, from $72,000/yr), stacked under the price. Bump the price-slot height to keep rows aligned across tiers. - Remove the "Custom Integration & Policy Design" service package; the remaining four fixed-scope services stand on their own. --- .vitepress/theme/Pricing.vue | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.vitepress/theme/Pricing.vue b/.vitepress/theme/Pricing.vue index 3486efa5..749957f7 100644 --- a/.vitepress/theme/Pricing.vue +++ b/.vitepress/theme/Pricing.vue @@ -19,7 +19,8 @@ const tiers = [ { name: 'Production', price: '$1,500', - cadence: '/mo, billed annually ($18,000/yr)', + cadence: '/mo, billed annually', + annual: '$18,000/yr', badge: 'Introductory pricing', forWho: 'One production workload', featured: true, @@ -36,7 +37,8 @@ const tiers = [ { name: 'Enterprise', price: 'From $6,000', - cadence: '/mo, billed annually (from $72,000/yr)', + cadence: '/mo, billed annually', + annual: 'from $72,000/yr', forWho: 'Mission-critical, regulated, or multiple workloads', featured: false, features: [ @@ -75,13 +77,6 @@ const services = [ detail: 'Map Cycles-generated evidence and runtime controls to selected control narratives for EU AI Act readiness, NIST AI RMF, and ISO/IEC 42001. Configure CyclesEvidence signing and retention/cold export, and deliver an auditor-ready evidence pack. This is not legal advice.', }, - { - name: 'Custom Integration & Policy Design', - price: 'From $250', - cadence: '/hr or fixed bid', - detail: - 'Custom SDK or agent-host integration, policy and scope design, and migrations — scoped to your stack and delivered as a working patch set.', - }, { name: 'Team Enablement Workshop', price: '$3,500', @@ -112,6 +107,7 @@ const services = [
{{ tier.price }} {{ tier.cadence }} + {{ tier.annual }}

{{ tier.forWho }}

    @@ -241,7 +237,7 @@ const services = [ /* Fixed-height price slot so the description and CTA rows share a baseline across all three tiers regardless of how the cadence text wraps. */ .tier .price-row { - min-height: 72px; + min-height: 84px; justify-content: flex-start; } @@ -263,6 +259,13 @@ const services = [ line-height: 1.4; } +.annual { + font-size: 13px; + font-weight: 600; + color: var(--vp-c-text-2); + line-height: 1.4; +} + .for-who { font-size: 14px; font-weight: 600; From ef5e90c385f56ed8e5c9ca17728f08f1ebf8191a Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 14:39:23 -0400 Subject: [PATCH 6/8] refactor: slim down top nav from 10 items to 6 + icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The top nav had grown busy and several items overlapped the Docs dropdown. Consolidate: - Fold the top-level Protocol link into Docs > Reference (as "Protocol Reference (Interactive)"). - Fold the top-level Tools dropdown into Docs as a "Tools" group. - Group Partners under a Contact dropdown (Talk to the team / Become a design partner). - Drop the redundant "GitHub" text item; the GitHub social icon already covers it, repointed from the docs repo to the org (github.com/runcycles). Top bar is now: Why Cycles · Quickstart · Docs · Pricing · Blog · Contact · [GitHub icon]. --- .vitepress/config.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index e89f7866..57c214ab 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -230,15 +230,6 @@ export default defineConfig({ nav: [ { text: 'Why Cycles', link: '/why-cycles' }, { text: 'Quickstart', link: '/quickstart/what-is-cycles' }, - { text: 'Protocol', link: '/protocol/' }, - { - text: 'Tools', - items: [ - { text: 'All tools', link: '/calculators/' }, - { text: 'Blast Radius Risk Calculator', link: '/calculators/ai-agent-blast-radius-standalone' }, - { text: 'Cost Calculator (Claude vs GPT)', link: '/calculators/claude-vs-gpt-cost-standalone' }, - ], - }, { text: 'Docs', items: [ @@ -258,6 +249,7 @@ export default defineConfig({ items: [ { text: 'API Reference (Interactive)', link: '/api/' }, { text: 'Admin API (Interactive)', link: '/admin-api/' }, + { text: 'Protocol Reference (Interactive)', link: '/protocol/' }, { text: 'Protocol Spec', link: 'https://github.com/runcycles/cycles-protocol' }, { text: 'Configuration', link: '/configuration/python-client-configuration-reference' }, ], @@ -270,13 +262,25 @@ export default defineConfig({ { text: 'Demos', link: '/demos/' }, ], }, + { + text: 'Tools', + items: [ + { text: 'All tools', link: '/calculators/' }, + { text: 'Blast Radius Risk Calculator', link: '/calculators/ai-agent-blast-radius-standalone' }, + { text: 'Cost Calculator (Claude vs GPT)', link: '/calculators/claude-vs-gpt-cost-standalone' }, + ], + }, ], }, - { text: 'Blog', link: '/blog/' }, { text: 'Pricing', link: '/pricing' }, - { text: 'Partners', link: '/design-partners' }, - { text: 'Contact', link: '/contact' }, - { text: 'GitHub', link: 'https://github.com/runcycles' } + { text: 'Blog', link: '/blog/' }, + { + text: 'Contact', + items: [ + { text: 'Talk to the team', link: '/contact' }, + { text: 'Become a design partner', link: '/design-partners' }, + ], + }, ], sidebar: { '/api/': [ @@ -609,7 +613,7 @@ export default defineConfig({ ], }, socialLinks: [ - { icon: 'github', link: 'https://github.com/runcycles/cycles-docs' } + { icon: 'github', link: 'https://github.com/runcycles' } ] }, transformPageData(pageData) { From e417293075225313d3c609fc367830c76b8f4b05 Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 14:42:45 -0400 Subject: [PATCH 7/8] refactor: keep Tools as a top-level nav dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move Tools back out of the Docs dropdown to its own top-level item, per preference. Protocol stays under Docs > Reference. Top bar: Why Cycles · Quickstart · Docs · Tools · Pricing · Blog · Contact · [GitHub icon]. --- .vitepress/config.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.vitepress/config.ts b/.vitepress/config.ts index 57c214ab..618b430b 100644 --- a/.vitepress/config.ts +++ b/.vitepress/config.ts @@ -262,14 +262,14 @@ export default defineConfig({ { text: 'Demos', link: '/demos/' }, ], }, - { - text: 'Tools', - items: [ - { text: 'All tools', link: '/calculators/' }, - { text: 'Blast Radius Risk Calculator', link: '/calculators/ai-agent-blast-radius-standalone' }, - { text: 'Cost Calculator (Claude vs GPT)', link: '/calculators/claude-vs-gpt-cost-standalone' }, - ], - }, + ], + }, + { + text: 'Tools', + items: [ + { text: 'All tools', link: '/calculators/' }, + { text: 'Blast Radius Risk Calculator', link: '/calculators/ai-agent-blast-radius-standalone' }, + { text: 'Cost Calculator (Claude vs GPT)', link: '/calculators/claude-vs-gpt-cost-standalone' }, ], }, { text: 'Pricing', link: '/pricing' }, From 11105df2f8e1b9bf29651750d2b851a9350e088b Mon Sep 17 00:00:00 2001 From: Albert Mavashev Date: Mon, 15 Jun 2026 15:07:29 -0400 Subject: [PATCH 8/8] fix: pricing button hover, ribbon copy, and card alignment in markdown - Ribbon: "Most popular" -> "Recommended" (honest for a pre-revenue, design-partner-stage product). - Hover bug: CTA buttons render inside .vp-doc, whose `a:hover` rule recolored the text (teal on teal = invisible). Pin color and text-decoration on hover for .cta-button and .cta-button.alt in both Pricing.vue and HomeContact.vue. Adaptive `color: var(--vp-c-bg)` keeps contrast correct in light (white on deep teal) and dark (dark on bright teal) themes. - Alignment: zero `margin` on .reason and .features li so VitePress's `.vp-doc li + li { margin-top }` can't offset cards/list items when these components are rendered inside markdown (the HomeContact "reasons" cards no longer sit lower than their row neighbors). --- .vitepress/theme/HomeContact.vue | 3 +++ .vitepress/theme/Pricing.vue | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.vitepress/theme/HomeContact.vue b/.vitepress/theme/HomeContact.vue index deca4849..a251ae46 100644 --- a/.vitepress/theme/HomeContact.vue +++ b/.vitepress/theme/HomeContact.vue @@ -123,6 +123,7 @@ const issuesUrl = 'https://github.com/runcycles/cycles-docs/issues' .reason { display: flex; flex-direction: column; + margin: 0; /* override .vp-doc `li + li` margin when rendered inside markdown */ padding: 16px 18px; border: 1px solid var(--vp-c-divider); border-radius: 10px; @@ -181,6 +182,8 @@ const issuesUrl = 'https://github.com/runcycles/cycles-docs/issues' .cta-button:hover { background: var(--vp-c-brand-2); + color: var(--vp-c-bg); + text-decoration: none; } .cta-button:active { diff --git a/.vitepress/theme/Pricing.vue b/.vitepress/theme/Pricing.vue index 749957f7..ceafaf5a 100644 --- a/.vitepress/theme/Pricing.vue +++ b/.vitepress/theme/Pricing.vue @@ -97,7 +97,7 @@ const services = [ class="card tier" :class="{ featured: tier.featured }" > - Most popular + Recommended

    {{ tier.name }}

    @@ -287,6 +287,7 @@ const services = [ .features li { position: relative; + margin: 0; /* override .vp-doc `li + li` margin inside markdown; spacing comes from the flex gap */ padding-left: 22px; font-size: 14px; color: var(--vp-c-text-2); @@ -318,7 +319,13 @@ const services = [ transition: background 0.2s, transform 0.1s; } -.cta-button:hover { background: var(--vp-c-brand-2); } +/* Pin color/decoration on hover: these render inside .vp-doc, whose + `a:hover` rule would otherwise recolor the text (teal on teal = invisible). */ +.cta-button:hover { + background: var(--vp-c-brand-2); + color: var(--vp-c-bg); + text-decoration: none; +} .cta-button:active { transform: translateY(1px); } .cta-button.alt { @@ -330,6 +337,8 @@ const services = [ .cta-button.alt:hover { background: var(--vp-c-bg); border-color: var(--vp-c-brand-1); + color: var(--vp-c-brand-1); + text-decoration: none; } .boundary {