feat(ui): rewrite marketing/donate pages on clean shadcn, delete their CSS (fast mode) (#823)

Rewrites the static marketing/info surfaces on shadcn primitives (Card / Button /
Badge) + Tailwind token utilities, dropping the three legacy page stylesheets.
FAST mode: clean shadcn + Tailwind defaults, palette kept (via the existing
color-mix + var(--chrome-*) arbitrary utilities), behavior intact, not
pixel-perfect.

- SupportPage.jsx (SupportView=donate + LicenseView=enterprise): hero, segmented
  Support/License toggle, Fund-Claude-Max goal Card, amount picker, Ko-fi/PayPal
  link cards, benefit Cards, and the per-deployment quote panel — all on
  Card/Button/Badge + Tailwind. All i18n keys, URLs, openExternal, amount state,
  and view toggling preserved.
- ContactPage.jsx: hero + Discord/Email/Issues/Website channel cards rebuilt as
  hue-tinted Tailwind link rows.
- Deleted DonatePage.css (282), EnterprisePage.css (233), SupportPage.css (140)
  = 655 lines removed; no JS imports them anymore.

Kept (shared, untouched): index.css `.lp-aurora*` + `.lp-hero__sweep` (also used
by Launchpad). Left the donate widgets (GoalBar/Pip/Postcard) and their
DonateGoal.css/Postcard.css in place — already Tailwind-based with genuinely
irreducible keyframes (goal-fill grow, Pip bob/wave, postcard stamp/perforation),
the sanctioned "small co-located keyframe CSS" exception. The dead no-op
`lp-glow-card` class (never defined in CSS) was dropped.

Verified: vite build ✓, oxlint 0, oxfmt clean, vitest 641 pass, visual 48 pass,
bun install --frozen-lockfile ✓. Eyeballed Donate/Enterprise/Support + Contact in
chromium against a stubbed backend — all coherent and on-palette.

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-07-01 02:28:38 +05:30
committed by GitHub
co-authored by mergetest Claude Opus 4.8
parent c931d8f4f8
commit 60ac321a1b
5 changed files with 228 additions and 833 deletions
+45 -47
View File
@@ -3,8 +3,6 @@ import { useTranslation } from 'react-i18next';
import { ArrowLeft, ExternalLink, MessageCircle, Mail, Bug, Globe } from 'lucide-react';
import { Button } from '../ui';
import { openExternal } from '../api/external';
import './DonatePage.css';
import './SupportPage.css';
// All outward contact channels in one place (#contact). Values are the same
// ones used across the app (donate/license footers) so there's a single source.
@@ -68,31 +66,34 @@ export default function ContactPage({ onBack }) {
const { t } = useTranslation();
return (
<div className="support-page donate-page flex flex-1 flex-col overflow-y-auto relative isolate bg-[var(--chrome-bg)]">
<div className="relative isolate flex flex-1 flex-col overflow-y-auto bg-[var(--chrome-bg)]">
<div className="lp-aurora" aria-hidden="true">
<span className="lp-aurora__blob lp-aurora__blob--pink" />
<span className="lp-aurora__blob lp-aurora__blob--green" />
<span className="lp-aurora__blob lp-aurora__blob--amber" />
</div>
<div className="support-page__topbar relative z-[2] flex items-center justify-between gap-[12px] pt-[16px] px-[44px]">
<div className="relative z-[2] flex items-center justify-between gap-3 px-11 pt-4">
<Button variant="subtle" size="sm" onClick={onBack} leading={<ArrowLeft size={14} />}>
{t('donate.back')}
</Button>
<span className="support-page__spacer w-[96px] shrink-0" aria-hidden="true" />
<span className="w-24 shrink-0" aria-hidden="true" />
</div>
<div className="support-page__content donate-page__content support-page__content--support relative z-[1] mx-auto flex w-full max-w-[640px] flex-col gap-[24px] px-[32px] pb-[40px]">
<div className="support-view">
<div className="donate-hero">
<div className="donate-hero__icon-wrap">
<MessageCircle size={24} className="donate-hero__heart" />
</div>
<h2 className="donate-hero__title">
<div className="relative z-[1] mx-auto flex w-full max-w-[640px] flex-1 flex-col justify-center gap-6 px-8 pb-10">
<div className="flex flex-col gap-6">
<div className="text-center">
<span className="mx-auto mb-4 flex size-12 items-center justify-center rounded-md border border-[color-mix(in_srgb,#d3869b_30%,transparent)] bg-[color-mix(in_srgb,#d3869b_12%,transparent)]">
<MessageCircle
size={24}
className="text-[#f3a5b6] drop-shadow-[0_0_12px_rgba(243,165,182,0.5)]"
/>
</span>
<h2 className="relative inline-block font-serif text-[2rem] font-normal leading-tight tracking-[-0.02em] text-[var(--chrome-fg)]">
{t('contact.hero_title', { defaultValue: 'Get in touch' })}
<span className="lp-hero__sweep" aria-hidden="true" />
</h2>
<p className="donate-hero__subtitle mx-auto mt-[10px] max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
<p className="mx-auto mt-2.5 max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
{t('contact.hero_desc', {
defaultValue:
'Questions, bugs, licensing, or just to say hi — heres how to reach me.',
@@ -100,40 +101,37 @@ export default function ContactPage({ onBack }) {
</p>
</div>
<section className="donate-section">
<div className="donate-grid support-methods grid grid-cols-[1fr] gap-[10px]">
{CHANNELS.map((c, i) => {
const Icon = c.icon;
return (
<button
key={c.id}
type="button"
className="donate-card donate-card--link lp-glow-card"
style={{ '--anim-i': i, '--card-hue': c.hue }}
onClick={() => openExternal(c.url)}
>
<span className="donate-card__glow" aria-hidden="true" />
<div className="donate-card__icon">
<Icon size={20} />
</div>
<div className="donate-card__body">
<div className="donate-card__label">
{t(c.labelKey, { defaultValue: c.labelDefault })}
</div>
<div className="donate-card__desc">
{t(c.descKey, { defaultValue: c.descDefault })}
</div>
<div className="contact-card__value mt-[4px] font-mono text-[11px] text-[var(--text-muted,#928374)] opacity-[0.85] [word-break:break-all]">
{c.value}
</div>
</div>
<div className="donate-card__arrow">
<ExternalLink size={14} />
</div>
</button>
);
})}
</div>
<section className="grid grid-cols-1 gap-2.5">
{CHANNELS.map((c) => {
const Icon = c.icon;
return (
<button
key={c.id}
type="button"
onClick={() => openExternal(c.url)}
style={{ '--card-hue': c.hue }}
className="flex w-full items-center gap-3 overflow-hidden rounded-md border border-border bg-transparent px-3.5 py-2.5 text-left transition-colors hover:border-[color-mix(in_srgb,var(--card-hue)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--card-hue)_6%,transparent)]"
>
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-[color-mix(in_srgb,var(--card-hue)_22%,transparent)] bg-[color-mix(in_srgb,var(--card-hue)_10%,transparent)]">
<Icon size={20} />
</span>
<span className="min-w-0 flex-1">
<span className="block font-mono text-xs font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
{t(c.labelKey, { defaultValue: c.labelDefault })}
</span>
<span className="block font-sans text-[0.68rem] leading-snug text-[var(--chrome-fg-muted)]">
{t(c.descKey, { defaultValue: c.descDefault })}
</span>
<span className="mt-1 block break-all font-mono text-[11px] text-[var(--chrome-fg-muted)] opacity-85">
{c.value}
</span>
</span>
<span className="flex size-7 shrink-0 items-center justify-center rounded-md border border-border text-[var(--chrome-fg-muted)]">
<ExternalLink size={14} />
</span>
</button>
);
})}
</section>
</div>
</div>
-282
View File
@@ -1,282 +0,0 @@
/* DonatePage — same chrome frame + aurora system as the Launchpad.
Uses design tokens from index.css (:root) and reuses .lp-aurora,
.lp-aurora__blob, .lp-hero__sweep, .lp-glow-card from the Launchpad
styles so there's zero visual divergence. */
/* .donate-page + .donate-page__content + .donate-hero__subtitle + .donate-footer
+ .donate-social-proof (base) + .donate-amounts are now Tailwind utilities in
SupportPage.jsx / ContactPage.jsx. Everything below stays as CSS — it carries
keyframes, ::before/::after dividers, :hover combinators, color-mix accents,
the --card-hue/--anim-i custom props, and the reduced-motion overrides. */
/* ── Top bar (Back + Commercial License) ──────────────────── */
.donate-page__topbar {
padding: 16px 44px 0;
position: relative;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
/* ── Hero ──────────────────────────────────────────────────── */
.donate-hero {
text-align: center;
padding-top: 24px;
animation: donateIn 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.donate-hero__icon-wrap {
width: 48px;
height: 48px;
margin: 0 auto 16px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, #d3869b 12%, transparent);
border: 1px solid color-mix(in srgb, #d3869b 30%, transparent);
box-shadow: 0 0 32px -4px rgba(211, 134, 155, 0.2);
}
.donate-hero__heart {
color: #f3a5b6;
fill: rgba(243, 165, 182, 0.35);
filter: drop-shadow(0 0 12px rgba(243, 165, 182, 0.5));
animation: donateHeartPulse 3s ease-in-out infinite;
}
@keyframes donateHeartPulse {
0%, 100% { transform: scale(1); filter: drop-shadow(0 0 12px rgba(243, 165, 182, 0.4)); }
50% { transform: scale(1.08); filter: drop-shadow(0 0 20px rgba(243, 165, 182, 0.7)); }
}
.donate-hero__title {
font-family: var(--font-serif);
font-size: 2.0rem;
font-weight: 400;
letter-spacing: -0.02em;
color: var(--chrome-fg);
margin: 0;
position: relative;
display: inline-block;
line-height: 1.1;
}
/* ── Section dividers ─────────────────────────────────────── */
.donate-section__title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.donate-section__title::before,
.donate-section__title::after {
content: "";
flex: 1;
height: 1px;
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
background-size: 6px 1px;
}
.donate-section__title span {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--chrome-label-track);
color: var(--chrome-fg-muted);
white-space: nowrap;
}
/* ── Card grid ─────────────────────────────────────────────── */
.donate-grid--links {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 10px;
}
/* ── Shared card chrome ───────────────────────────────────── */
.donate-card {
position: relative;
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: transparent;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
font: inherit;
color: inherit;
text-align: left;
cursor: pointer;
overflow: hidden;
isolation: isolate;
transition: background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-base);
animation: donateCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
animation-delay: calc(var(--anim-i, 0) * 80ms);
}
.donate-card:hover {
transform: translateY(-1px);
background: color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 6%, transparent);
border-color: color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 40%, transparent);
box-shadow: 0 10px 24px -12px color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 35%, transparent);
}
@keyframes donateCardIn {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
/* Glow layer — reuses the Launchpad's breath/spotlight pattern */
.donate-card__glow {
position: absolute;
inset: 0;
border-radius: inherit;
overflow: hidden;
pointer-events: none;
z-index: 0;
border: 1px solid color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 25%, transparent);
opacity: 0.15;
animation: lpBreath 5.2s ease-in-out infinite;
animation-delay: calc(var(--anim-i, 0) * -1.2s);
}
/* Icon bubble */
.donate-card__icon {
width: 32px;
height: 32px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 22%, transparent);
font-size: 1.1rem;
position: relative;
z-index: 1;
transition: transform var(--dur-base);
}
.donate-card:hover .donate-card__icon {
transform: scale(1.06);
}
/* Body text */
.donate-card__body {
flex: 1;
min-width: 0;
position: relative;
z-index: 1;
}
.donate-card__label {
font-family: var(--chrome-font-mono);
font-size: 0.75rem;
font-weight: 600;
color: var(--chrome-fg);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
}
.donate-card__desc {
font-family: var(--font-sans);
font-size: 0.68rem;
color: var(--chrome-fg-muted);
margin-top: 0;
line-height: 1.45;
}
/* Arrow for link cards */
.donate-card__arrow {
flex-shrink: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--chrome-radius-pill);
border: 1px solid var(--chrome-border);
color: var(--chrome-fg-muted);
transition: border-color var(--dur-fast), color var(--dur-fast), background var(--dur-fast);
position: relative;
z-index: 1;
}
.donate-card:hover .donate-card__arrow {
border-color: color-mix(in srgb, var(--card-hue, #d3869b) 45%, transparent);
color: var(--card-hue, #d3869b);
background: color-mix(in srgb, var(--card-hue, #d3869b) 8%, transparent);
}
/* ── "Fund Claude Max" goal section + social proof ─────────── */
.donate-goal-section {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px 18px;
border: 1px solid var(--chrome-border);
border-radius: 10px;
background: color-mix(in srgb, var(--chrome-accent) 4%, transparent);
animation: donateCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.donate-social-proof svg { color: var(--chrome-accent); }
/* ── Suggested amounts ─────────────────────────────────────── */
.donate-amount {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
min-height: 52px;
padding: 8px 6px;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
background: transparent;
color: var(--chrome-fg);
font: inherit;
cursor: pointer;
transition: background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-base);
}
.donate-amount:hover {
transform: translateY(-1px);
background: color-mix(in srgb, var(--chrome-accent) 7%, transparent);
border-color: color-mix(in srgb, var(--chrome-accent) 40%, transparent);
}
.donate-amount.is-selected {
border-color: var(--chrome-accent);
background: var(--chrome-accent-bg);
}
.donate-amount__value {
font-family: var(--font-serif);
font-size: 1.05rem;
font-weight: 500;
color: var(--chrome-fg);
}
.donate-amount--common { border-color: color-mix(in srgb, var(--chrome-accent) 35%, transparent); }
.donate-amount__badge {
font-family: var(--chrome-font-mono);
font-size: 0.54rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--chrome-accent);
white-space: nowrap;
}
.donate-amount--custom .donate-amount__value {
font-family: var(--chrome-font-mono);
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: var(--chrome-label-track, 0.08em);
color: var(--chrome-fg-muted);
}
/* ── Entry animation ──────────────────────────────────────── */
@keyframes donateIn {
0% { opacity: 0; transform: translateY(16px); }
100% { opacity: 1; transform: translateY(0); }
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
.donate-hero,
.donate-card,
.donate-hero__heart,
.donate-card__glow,
.donate-goal-section,
.donate-amount {
animation: none !important;
}
.donate-amount:hover { transform: none; }
}
-233
View File
@@ -1,233 +0,0 @@
/* EnterprisePage — chrome design system, same visual language as
Launchpad and DonatePage. Reuses .lp-aurora, .lp-hero__sweep. */
.enterprise-page {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
background: var(--chrome-bg);
position: relative;
isolation: isolate;
}
.enterprise-page__back {
padding: 16px 44px 0;
position: relative;
z-index: 2;
}
.enterprise-page__content {
position: relative;
z-index: 1;
max-width: 900px;
width: 100%;
margin: 0 auto;
padding: 0 32px 56px;
display: flex;
flex-direction: column;
gap: 40px;
}
/* ── Hero ──────────────────────────────────────────────────── */
.ent-hero {
text-align: center;
padding-top: 32px;
animation: entIn 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
}
/* .ent-hero__kicker / .ent-hero__subtitle (base) / .ent-why__grid /
.ent-why__label / .ent-why__desc are now Tailwind utilities in
SupportPage.jsx (LicenseView). The rest stays — keyframes, ::before/::after
dividers, :hover combinators, color-mix accents, !important, details markers. */
.ent-hero__title {
font-family: var(--font-serif);
font-size: 2.4rem;
font-weight: 400;
letter-spacing: -0.02em;
color: var(--chrome-fg);
margin: 10px 0 0;
position: relative;
display: inline-block;
line-height: 1.1;
}
.ent-hero__subtitle strong {
color: var(--chrome-fg);
font-weight: 600;
}
/* ── Section title (dotted line + label) ──────────────────── */
.ent-section-title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.ent-section-title::before,
.ent-section-title::after {
content: "";
flex: 1;
height: 1px;
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
background-size: 6px 1px;
}
.ent-section-title span {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
font-weight: 600;
text-transform: uppercase;
letter-spacing: var(--chrome-label-track);
color: var(--chrome-fg-muted);
white-space: nowrap;
}
/* ── Why grid ─────────────────────────────────────────────── */
.ent-why__card {
padding: 14px 16px;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
transition: background var(--dur-fast), border-color var(--dur-fast);
animation: entCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.ent-why__card:hover {
background: var(--chrome-hover-bg);
border-color: var(--chrome-border-strong);
}
.ent-why__icon {
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, #d3869b 10%, transparent);
border: 1px solid color-mix(in srgb, #d3869b 22%, transparent);
color: #d3869b;
margin-bottom: 10px;
}
/* ── Pricing — coming-soon panel ──────────────────────────── */
.ent-coming-soon {
display: flex;
flex-direction: column;
gap: 14px;
padding: 24px 22px;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, #fe8019 5%, transparent);
text-align: center;
align-items: center;
animation: entCardIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.ent-coming-soon p {
margin: 0;
max-width: 540px;
color: var(--chrome-fg-muted);
line-height: 1.5;
}
.ent-coming-soon__cta {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 18px;
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, #fe8019 18%, transparent);
border: 1px solid color-mix(in srgb, #fe8019 50%, transparent);
color: var(--chrome-fg);
font-weight: 600;
cursor: pointer;
transition: background var(--dur-fast), border-color var(--dur-fast), transform var(--dur-fast);
}
.ent-coming-soon__cta:hover {
background: color-mix(in srgb, #fe8019 28%, transparent);
border-color: color-mix(in srgb, #fe8019 70%, transparent);
transform: translateY(-1px);
}
/* ── FAQ ──────────────────────────────────────────────────── */
.ent-faq__list {
display: flex;
flex-direction: column;
gap: 6px;
}
.ent-faq__item {
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
overflow: hidden;
}
.ent-faq__item summary {
padding: 10px 16px;
font-family: var(--font-sans);
font-size: 0.78rem;
font-weight: 600;
color: var(--chrome-fg);
cursor: pointer;
list-style: none;
transition: background var(--dur-fast);
}
.ent-faq__item summary::-webkit-details-marker { display: none; }
.ent-faq__item summary::before {
content: "▸";
display: inline-block;
margin-right: 10px;
font-size: 0.65rem;
color: var(--chrome-fg-dim);
transition: transform var(--dur-fast);
}
.ent-faq__item[open] summary::before {
transform: rotate(90deg);
}
.ent-faq__item summary:hover {
background: var(--chrome-hover-bg);
}
.ent-faq__item p {
margin: 0;
padding: 0 16px 12px 32px;
font-family: var(--font-sans);
font-size: 0.72rem;
color: var(--chrome-fg-muted);
line-height: 1.6;
}
/* ── CTA Footer ───────────────────────────────────────────── */
.ent-cta-footer {
text-align: center;
padding-bottom: 20px;
}
.ent-cta-footer p {
font-family: var(--chrome-font-mono);
font-size: 0.72rem;
color: var(--chrome-fg-muted);
margin: 0;
}
.ent-cta-footer__link {
background: none;
border: none;
font: inherit;
color: var(--chrome-accent);
cursor: pointer;
padding: 0;
}
.ent-cta-footer__link:hover {
text-decoration: underline;
}
.ent-cta-footer__sub {
margin-top: 6px !important;
font-size: 0.65rem !important;
color: var(--chrome-fg-dim) !important;
}
/* ── Animations ───────────────────────────────────────────── */
@keyframes entIn {
0% { opacity: 0; transform: translateY(16px); }
100% { opacity: 1; transform: translateY(0); }
}
@keyframes entCardIn {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.ent-hero,
.ent-why__card,
.ent-tier {
animation: none !important;
}
}
-140
View File
@@ -1,140 +0,0 @@
/* SupportPage — unifies the donate + commercial-license panels behind one
segmented toggle. Reuses the donate/enterprise chrome (DonatePage.css,
EnterprisePage.css); this file only owns the toggle + panel transitions. */
/* `.support-page__topbar` base + `.support-page__spacer` base are now Tailwind
utilities in SupportPage.jsx / ContactPage.jsx; their @media (max-width:540px)
overrides stay below. */
/* ── Segmented toggle ─────────────────────────────────────────────────── */
.support-toggle {
position: relative;
display: grid;
grid-template-columns: 1fr 1fr;
padding: 3px;
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, var(--chrome-fg) 5%, transparent);
border: 1px solid var(--chrome-border);
isolation: isolate;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.25);
}
/* The sliding active background. Carries the panel's accent hue so the toggle
itself signals which world you're in — pink for Support, teal for License. */
.support-toggle__pill {
position: absolute;
top: 3px;
bottom: 3px;
left: 3px;
width: calc(50% - 3px);
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, #d3869b 18%, transparent);
border: 1px solid color-mix(in srgb, #d3869b 38%, transparent);
box-shadow: 0 2px 14px -5px rgba(211, 134, 155, 0.55);
z-index: 0;
transition:
transform 0.36s cubic-bezier(0.16, 1, 0.3, 1),
background 0.36s ease,
border-color 0.36s ease,
box-shadow 0.36s ease;
}
.support-toggle__pill[data-view="license"] {
transform: translateX(100%);
background: color-mix(in srgb, #83a598 18%, transparent);
border-color: color-mix(in srgb, #83a598 40%, transparent);
box-shadow: 0 2px 14px -5px rgba(131, 165, 152, 0.55);
}
.support-toggle__opt {
position: relative;
z-index: 1;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 18px;
border: none;
background: transparent;
font-family: var(--chrome-font-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
white-space: nowrap;
color: var(--chrome-fg-muted);
cursor: pointer;
border-radius: var(--chrome-radius-pill);
transition: color 0.22s ease;
}
.support-toggle__opt svg { transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); }
.support-toggle__opt.is-active { color: var(--chrome-fg); }
.support-toggle__opt.is-active svg { transform: scale(1.12); }
.support-toggle__opt:hover:not(.is-active) { color: var(--chrome-fg); }
.support-toggle__opt:focus-visible {
outline: 2px solid color-mix(in srgb, var(--chrome-accent) 60%, transparent);
outline-offset: 2px;
}
/* ── Panel cross-fade — the content remounts on toggle (key={view}), which
replays the inner hero/card entry animations; this adds a soft lift over
the whole panel so the swap feels intentional, not jumpy. ─────────────── */
.support-view {
animation: supportViewIn 0.42s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes supportViewIn {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
/* ── Support panel layout ─────────────────────────────────────────────────
The Support panel is short, so center it vertically in the available space
instead of letting it cling to the top of an empty page. License is tall
enough to fill on its own, so it stays top-aligned and scrolls normally. */
.support-page__content--support {
flex: 1 1 auto;
justify-content: center;
}
/* `.support-methods` (single-column grid) + `.support-chips` (centered flex
wrap) are now Tailwind utilities in the JSX. `.support-chip` stays — it has
:hover state + transitions + a descendant svg color. */
.support-chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, var(--chrome-fg) 4%, transparent);
color: var(--chrome-fg-muted);
font-family: var(--chrome-font-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
cursor: pointer;
transition: color var(--dur-fast), border-color var(--dur-fast),
background var(--dur-fast), transform var(--dur-base);
}
.support-chip:hover {
color: var(--chrome-fg);
border-color: color-mix(in srgb, #d3869b 42%, transparent);
background: color-mix(in srgb, #d3869b 8%, transparent);
transform: translateY(-1px);
}
.support-chip svg { color: #d3869b; }
@media (max-width: 540px) {
.support-page__topbar { padding: 14px 18px 0; }
.support-page__spacer { display: none; }
.support-toggle__opt { padding: 6px 12px; }
}
@media (prefers-reduced-motion: reduce) {
.support-toggle__pill,
.support-toggle__opt svg { transition: none; }
.support-view { animation: none; }
}
/* `.contact-card__value` (the small monospace channel value under each Contact
card) is now a Tailwind utility set in ContactPage.jsx. */
+183 -131
View File
@@ -13,13 +13,11 @@ import {
Star,
MessageCircle,
} from 'lucide-react';
import { Button } from '../ui';
import { Button, Badge } from '../ui';
import { Card } from '@/components/ui/card';
import { openExternal } from '../api/external';
import GoalBar from '../components/donate/GoalBar';
import { loadDonationProgress, BUNDLED_PROGRESS } from '../api/donation';
import './DonatePage.css';
import './EnterprisePage.css';
import './SupportPage.css';
// GitHub Sponsors isn't available, so donations go through Ko-fi or PayPal and
// the supporter picks which — no default-charge nudge, none pre-selected.
@@ -43,6 +41,8 @@ const METHODS = [
},
];
const DONATE_HUE = '#d3869b';
// PayPal.me carries the chosen amount straight into the checkout; Ko-fi opens
// its tip page (no reliable preset-amount URL). A non-numeric/"custom" amount
// falls back to the bare link.
@@ -51,28 +51,51 @@ function methodUrl(method, amount) {
return method.url;
}
function LinkCard({ method, amount, style }) {
const { t } = useTranslation();
// Shared "rich link" card — a clickable row with an icon bubble, title + body,
// and a trailing external-link affordance. `hue` tints the icon bubble + hover.
function LinkCard({ icon, label, desc, value, hue, onClick }) {
return (
<button
type="button"
className="donate-card donate-card--link lp-glow-card"
style={style}
onClick={() => openExternal(methodUrl(method, amount))}
onClick={onClick}
style={{ '--card-hue': hue }}
className="flex w-full items-center gap-3 overflow-hidden rounded-md border border-border bg-transparent px-3.5 py-2.5 text-left transition-colors hover:border-[color-mix(in_srgb,var(--card-hue)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--card-hue)_6%,transparent)]"
>
<span className="donate-card__glow" aria-hidden="true" />
<div className="donate-card__icon">{method.icon}</div>
<div className="donate-card__body">
<div className="donate-card__label">{method.label}</div>
<div className="donate-card__desc">{t(method.descriptionKey)}</div>
</div>
<div className="donate-card__arrow">
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-[color-mix(in_srgb,var(--card-hue)_22%,transparent)] bg-[color-mix(in_srgb,var(--card-hue)_10%,transparent)] text-[1.1rem]">
{icon}
</span>
<span className="min-w-0 flex-1">
<span className="block font-mono text-xs font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
{label}
</span>
<span className="block font-sans text-[0.68rem] leading-snug text-[var(--chrome-fg-muted)]">
{desc}
</span>
{value && (
<span className="mt-1 block break-all font-mono text-[11px] text-[var(--chrome-fg-muted)] opacity-85">
{value}
</span>
)}
</span>
<span className="flex size-7 shrink-0 items-center justify-center rounded-md border border-border text-[var(--chrome-fg-muted)]">
<ExternalLink size={14} />
</div>
</span>
</button>
);
}
// Section label: a mono uppercase caption with a trailing hairline.
function SectionTitle({ children }) {
return (
<div className="mb-2.5 flex items-center gap-3">
<span className="whitespace-nowrap font-mono text-[var(--chrome-label-size)] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
{children}
</span>
<span className="h-px flex-1 bg-border" />
</div>
);
}
/* ── Support (donate) panel ───────────────────────────────────────────── */
function SupportView() {
const { t } = useTranslation();
@@ -90,25 +113,28 @@ function SupportView() {
}, []);
return (
<div className="support-view">
<div className="donate-hero">
<div className="donate-hero__icon-wrap">
<Heart size={24} className="donate-hero__heart" />
</div>
<h2 className="donate-hero__title">
<div className="flex flex-col gap-6">
<div className="text-center">
<span className="mx-auto mb-4 flex size-12 items-center justify-center rounded-md border border-[color-mix(in_srgb,#d3869b_30%,transparent)] bg-[color-mix(in_srgb,#d3869b_12%,transparent)]">
<Heart
size={24}
className="text-[#f3a5b6] [fill:rgba(243,165,182,0.35)] drop-shadow-[0_0_12px_rgba(243,165,182,0.5)]"
/>
</span>
<h2 className="relative inline-block font-serif text-[2rem] font-normal leading-tight tracking-[-0.02em] text-[var(--chrome-fg)]">
{t('donate.hero_title')}
<span className="lp-hero__sweep" aria-hidden="true" />
</h2>
<p className="donate-hero__subtitle mx-auto mt-[10px] max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
<p className="mx-auto mt-2.5 max-w-[480px] font-sans text-[0.8rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
{t('donate.hero_desc')}
</p>
</div>
{/* ── "Fund Claude Max" goal bar + social proof ──────────────────── */}
<section className="donate-section donate-goal-section">
<Card className="gap-3 rounded-lg border-border bg-[color-mix(in_srgb,var(--chrome-accent)_4%,transparent)] p-[18px] py-4 shadow-none">
<GoalBar progress={progress} />
<div className="donate-social-proof flex items-center justify-center gap-[6px] font-mono text-[0.7rem] tracking-[0.01em] text-[var(--chrome-fg-muted)]">
<Users size={13} />
<div className="flex items-center justify-center gap-1.5 font-mono text-[0.7rem] tracking-[0.01em] text-[var(--chrome-fg-muted)]">
<Users size={13} className="text-[var(--chrome-accent)]" />
<span>
{t('donate.goal.social_proof', {
defaultValue: 'Join {{count}} supporters funding local AI',
@@ -116,43 +142,56 @@ function SupportView() {
})}
</span>
</div>
</section>
</Card>
{/* ── Step 1: pick an amount (none pre-selected; middle is "most common"). ──
Selecting only *records* the amount — the supporter then chooses
Ko-fi or PayPal below, and PayPal carries the amount through. */}
<section className="donate-section">
<div className="donate-section__title">
<span>{t('donate.suggested_title', { defaultValue: 'Pick an amount' })}</span>
</div>
<section>
<SectionTitle>
{t('donate.suggested_title', { defaultValue: 'Pick an amount' })}
</SectionTitle>
<div
className="donate-amounts grid grid-cols-[repeat(4,1fr)] gap-[8px]"
className="grid grid-cols-4 gap-2"
role="group"
aria-label={t('donate.suggested_title', { defaultValue: 'Pick an amount' })}
>
{SUGGESTED_AMOUNTS.map((a) => (
<button
key={a.value}
type="button"
className={`donate-amount ${amount === a.value ? 'is-selected' : ''} ${a.common ? 'donate-amount--common' : ''}`}
aria-pressed={amount === a.value}
onClick={() => setAmount(amount === a.value ? null : a.value)}
>
<span className="donate-amount__value">{a.label}</span>
{a.common && (
<span className="donate-amount__badge">
{t('donate.most_common', { defaultValue: 'most common' })}
{SUGGESTED_AMOUNTS.map((a) => {
const selected = amount === a.value;
return (
<button
key={a.value}
type="button"
aria-pressed={selected}
onClick={() => setAmount(selected ? null : a.value)}
className={`flex min-h-[52px] flex-col items-center justify-center gap-0.5 rounded-md border px-1.5 py-2 transition-colors ${
selected
? 'border-[var(--chrome-accent)] bg-[var(--chrome-accent-bg)]'
: `${a.common ? 'border-[color-mix(in_srgb,var(--chrome-accent)_35%,transparent)]' : 'border-border'} hover:border-[color-mix(in_srgb,var(--chrome-accent)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--chrome-accent)_7%,transparent)]`
}`}
>
<span className="font-serif text-[1.05rem] font-medium text-[var(--chrome-fg)]">
{a.label}
</span>
)}
</button>
))}
{a.common && (
<Badge tone="brand" size="xs" className="text-[0.54rem] tracking-[0.06em]">
{t('donate.most_common', { defaultValue: 'most common' })}
</Badge>
)}
</button>
);
})}
<button
type="button"
className={`donate-amount donate-amount--custom ${amount === 'custom' ? 'is-selected' : ''}`}
aria-pressed={amount === 'custom'}
onClick={() => setAmount(amount === 'custom' ? null : 'custom')}
className={`flex min-h-[52px] flex-col items-center justify-center gap-0.5 rounded-md border px-1.5 py-2 transition-colors ${
amount === 'custom'
? 'border-[var(--chrome-accent)] bg-[var(--chrome-accent-bg)]'
: 'border-border hover:border-[color-mix(in_srgb,var(--chrome-accent)_40%,transparent)] hover:bg-[color-mix(in_srgb,var(--chrome-accent)_7%,transparent)]'
}`}
>
<span className="donate-amount__value">
<span className="font-mono text-[0.78rem] uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
{t('donate.custom', { defaultValue: 'Custom' })}
</span>
</button>
@@ -161,24 +200,24 @@ function SupportView() {
{/* ── Step 2: pick where (Ko-fi or PayPal). GitHub Sponsors isn't
available; the supporter chooses, and PayPal carries the amount. ── */}
<section className="donate-section">
<div className="donate-section__title">
<span>
{typeof amount === 'number'
? t('donate.choose_method_amount', {
defaultValue: 'Continue with ${{amount}}',
amount,
})
: t('donate.choose_method', { defaultValue: 'Choose how to give' })}
</span>
</div>
<div className="donate-grid support-methods grid grid-cols-[1fr] gap-[10px]">
{METHODS.map((m, i) => (
<section>
<SectionTitle>
{typeof amount === 'number'
? t('donate.choose_method_amount', {
defaultValue: 'Continue with ${{amount}}',
amount,
})
: t('donate.choose_method', { defaultValue: 'Choose how to give' })}
</SectionTitle>
<div className="grid grid-cols-1 gap-2.5">
{METHODS.map((m) => (
<LinkCard
key={m.id}
method={m}
amount={typeof amount === 'number' ? amount : null}
style={{ '--anim-i': i, '--card-hue': '#d3869b' }}
icon={m.icon}
label={m.label}
desc={t(m.descriptionKey)}
hue={DONATE_HUE}
onClick={() => openExternal(methodUrl(m, typeof amount === 'number' ? amount : null))}
/>
))}
</div>
@@ -186,29 +225,29 @@ function SupportView() {
{/* Non-monetary ways to help — gives people who can't (or don't want to)
donate a real way to support, and balances out the panel. */}
<section className="donate-section">
<div className="donate-section__title">
<span>{t('support.other_ways')}</span>
</div>
<div className="support-chips flex flex-wrap justify-center gap-[10px]">
<button
type="button"
className="support-chip"
<section>
<SectionTitle>{t('support.other_ways')}</SectionTitle>
<div className="flex flex-wrap justify-center gap-2.5">
<Button
variant="subtle"
size="sm"
leading={<Star size={14} />}
onClick={() => openExternal('https://github.com/debpalash/OmniVoice-Studio')}
>
<Star size={14} /> {t('support.star_github')}
</button>
<button
type="button"
className="support-chip"
{t('support.star_github')}
</Button>
<Button
variant="subtle"
size="sm"
leading={<MessageCircle size={14} />}
onClick={() => openExternal('https://discord.gg/bzQavDfVV9')}
>
<MessageCircle size={14} /> {t('support.join_discord')}
</button>
{t('support.join_discord')}
</Button>
</div>
</section>
<div className="donate-footer pb-[20px] text-center font-mono text-[0.72rem] tracking-[0.02em] text-[var(--chrome-fg-dim)]">
<div className="pb-5 text-center font-mono text-[0.72rem] tracking-[0.02em] text-[var(--chrome-fg-dim)]">
{t('donate.footer')}
</div>
</div>
@@ -236,16 +275,16 @@ function LicenseView() {
},
];
return (
<div className="support-view">
<div className="ent-hero">
<span className="ent-hero__kicker font-mono text-[var(--chrome-label-size)] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg-muted)]">
<div className="flex flex-col gap-6">
<div className="text-center">
<Badge tone="neutral" size="sm">
{t('enterprise.badge')}
</span>
<h2 className="ent-hero__title">
</Badge>
<h2 className="relative mt-2.5 inline-block font-serif text-[2.4rem] font-normal leading-tight tracking-[-0.02em] text-[var(--chrome-fg)]">
{t('enterprise.hero_title')}
<span className="lp-hero__sweep" aria-hidden="true" />
</h2>
<p className="ent-hero__subtitle mx-auto mt-[16px] max-w-[540px] font-sans text-[0.85rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
<p className="mx-auto mt-4 max-w-[540px] font-sans text-[0.85rem] leading-[1.65] text-[var(--chrome-fg-muted)]">
{t('enterprise.hero_simple', {
defaultValue:
'OmniVoice Studio is free and open-source under the AGPL-3.0 — including for commercial and internal business use. You only need a commercial license to embed it in a closed-source product without AGPLs copyleft obligations.',
@@ -253,53 +292,52 @@ function LicenseView() {
</p>
</div>
<section className="ent-why">
<div className="ent-why__grid grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-[10px]">
{WHY_ITEMS.map(({ icon: Icon, label, desc }) => (
<div key={label} className="ent-why__card">
<div className="ent-why__icon">
<Icon size={16} />
</div>
<div className="ent-why__label mb-[4px] font-mono text-[0.75rem] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
{label}
</div>
<div className="ent-why__desc font-sans text-[0.72rem] leading-[1.5] text-[var(--chrome-fg-muted)]">
{desc}
</div>
<section className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-2.5">
{WHY_ITEMS.map(({ icon: Icon, label, desc }) => (
<Card
key={label}
className="gap-0 rounded-md border-border bg-transparent p-4 shadow-none transition-colors hover:border-border-strong hover:bg-[var(--chrome-hover-bg)]"
>
<span className="mb-2.5 flex size-[30px] items-center justify-center rounded-md border border-[color-mix(in_srgb,#d3869b_22%,transparent)] bg-[color-mix(in_srgb,#d3869b_10%,transparent)] text-[#d3869b]">
<Icon size={16} />
</span>
<div className="mb-1 font-mono text-[0.75rem] font-semibold uppercase tracking-[var(--chrome-label-track)] text-[var(--chrome-fg)]">
{label}
</div>
))}
</div>
<div className="font-sans text-[0.72rem] leading-[1.5] text-[var(--chrome-fg-muted)]">
{desc}
</div>
</Card>
))}
</section>
{/* One clear next step: pricing is quoted per deployment, so the action
is simply "tell me about your use case". */}
<section className="ent-tiers-section">
<div className="ent-coming-soon">
<p>
<section>
<Card className="items-center gap-3.5 rounded-md border-border bg-[color-mix(in_srgb,#fe8019_5%,transparent)] p-6 text-center shadow-none">
<p className="m-0 max-w-[540px] leading-[1.5] text-[var(--chrome-fg-muted)]">
{t('enterprise.contact_lead', {
defaultValue:
'Pricing is quoted per deployment so it fits your team and workload. Tell me your use case and Ill get you a quote.',
})}
</p>
<Button
variant="subtle"
leading={<Mail size={13} />}
onClick={() => openExternal(LICENSE_MAILTO)}
className="border-[color-mix(in_srgb,#fe8019_50%,transparent)] bg-[color-mix(in_srgb,#fe8019_18%,transparent)] font-semibold text-[var(--chrome-fg)] hover:border-[color-mix(in_srgb,#fe8019_70%,transparent)] hover:bg-[color-mix(in_srgb,#fe8019_28%,transparent)]"
>
{t('enterprise.request_quote')}
</Button>
<button
type="button"
className="ent-coming-soon__cta"
onClick={() => openExternal(LICENSE_MAILTO)}
title={LICENSE_EMAIL}
className="font-mono text-[0.65rem] text-[var(--chrome-accent)] hover:underline"
>
<Mail size={13} />
{t('enterprise.request_quote')}
{LICENSE_EMAIL}
</button>
<p className="ent-cta-footer__sub">
<button
type="button"
className="ent-cta-footer__link"
onClick={() => openExternal(LICENSE_MAILTO)}
title={LICENSE_EMAIL}
>
{LICENSE_EMAIL}
</button>
</p>
</div>
</Card>
</section>
</div>
);
@@ -316,8 +354,18 @@ export default function SupportPage({ onBack, initialView = 'support' }) {
const { t } = useTranslation();
const [view, setView] = useState(initialView === 'license' ? 'license' : 'support');
// Literal class strings (no interpolation) so Tailwind's JIT can see them.
const TAB_BASE =
'inline-flex items-center justify-center gap-1.5 rounded-md border px-[18px] py-1.5 font-mono text-[0.72rem] font-semibold uppercase tracking-[var(--chrome-label-track)] whitespace-nowrap transition-colors';
const TAB_INACTIVE =
'border-transparent text-[var(--chrome-fg-muted)] hover:text-[var(--chrome-fg)]';
const TAB_SUPPORT_ACTIVE =
'border-[color-mix(in_srgb,#d3869b_38%,transparent)] bg-[color-mix(in_srgb,#d3869b_18%,transparent)] text-[var(--chrome-fg)]';
const TAB_LICENSE_ACTIVE =
'border-[color-mix(in_srgb,#83a598_40%,transparent)] bg-[color-mix(in_srgb,#83a598_18%,transparent)] text-[var(--chrome-fg)]';
return (
<div className="support-page donate-page flex flex-1 flex-col overflow-y-auto relative isolate bg-[var(--chrome-bg)]">
<div className="relative isolate flex flex-1 flex-col overflow-y-auto bg-[var(--chrome-bg)]">
{/* Aurora backdrop — shared with the Launchpad */}
<div className="lp-aurora" aria-hidden="true">
<span className="lp-aurora__blob lp-aurora__blob--pink" />
@@ -326,18 +374,21 @@ export default function SupportPage({ onBack, initialView = 'support' }) {
</div>
{/* Top bar: Back (left) · toggle (center) · spacer (right, balances Back) */}
<div className="support-page__topbar relative z-[2] flex items-center justify-between gap-[12px] pt-[16px] px-[44px]">
<div className="relative z-[2] flex items-center justify-between gap-3 px-11 pt-4">
<Button variant="subtle" size="sm" onClick={onBack} leading={<ArrowLeft size={14} />}>
{t('donate.back')}
</Button>
<div className="support-toggle" role="tablist" aria-label={t('support.toggle_label')}>
<span className="support-toggle__pill" data-view={view} aria-hidden="true" />
<div
className="grid grid-cols-2 gap-1 rounded-md border border-border bg-[color-mix(in_srgb,var(--chrome-fg)_5%,transparent)] p-[3px]"
role="tablist"
aria-label={t('support.toggle_label')}
>
<button
type="button"
role="tab"
aria-selected={view === 'support'}
className={`support-toggle__opt ${view === 'support' ? 'is-active' : ''}`}
className={`${TAB_BASE} ${view === 'support' ? TAB_SUPPORT_ACTIVE : TAB_INACTIVE}`}
onClick={() => setView('support')}
>
<Heart size={13} /> {t('support.tab_support')}
@@ -346,22 +397,23 @@ export default function SupportPage({ onBack, initialView = 'support' }) {
type="button"
role="tab"
aria-selected={view === 'license'}
className={`support-toggle__opt ${view === 'license' ? 'is-active' : ''}`}
className={`${TAB_BASE} ${view === 'license' ? TAB_LICENSE_ACTIVE : TAB_INACTIVE}`}
onClick={() => setView('license')}
>
<Building2 size={13} /> {t('support.tab_license')}
</button>
</div>
<span className="support-page__spacer w-[96px] shrink-0" aria-hidden="true" />
<span className="w-24 shrink-0" aria-hidden="true" />
</div>
{/* key={view} remounts the panel so its entry animations replay on toggle.
The --support modifier vertically centers the (short) Support panel so
it doesn't float at the top of an empty page; License stays top-aligned
since it's tall enough to fill on its own. */}
The Support panel is short, so it's centered vertically; License is
tall enough to fill on its own and stays top-aligned. */}
<div
className={`support-page__content donate-page__content support-page__content--${view} relative z-[1] mx-auto flex w-full max-w-[640px] flex-col gap-[24px] px-[32px] pb-[40px]`}
className={`relative z-[1] mx-auto flex w-full max-w-[640px] flex-col gap-6 px-8 pb-10 ${
view === 'support' ? 'flex-1 justify-center' : ''
}`}
key={view}
>
{view === 'support' ? <SupportView /> : <LicenseView />}