diff --git a/docusaurus.config.js b/docusaurus.config.js index 6e6dd74..b115de7 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -198,6 +198,10 @@ const config = { href: 'https://atomgit.com/IvorySQL/IvorySQL', label: 'GitCode', }, + { + label: 'Contributors', + to: '/contributors', + }, ] }, { diff --git a/i18n/zh-CN/docusaurus-plugin-content-pages/community-page.md b/i18n/zh-CN/docusaurus-plugin-content-pages/community-page.md index 5fec0f9..7165b96 100644 --- a/i18n/zh-CN/docusaurus-plugin-content-pages/community-page.md +++ b/i18n/zh-CN/docusaurus-plugin-content-pages/community-page.md @@ -19,6 +19,9 @@ IvorySQL社区包括来自世界各地的开发和使用开源数据库的人员 - [提交bug](https://github.com/IvorySQL/IvorySQL/issues/new/choose) - [提交拉取请求](https://github.com/IvorySQL/IvorySQL/pulls) +## 贡献者墙 +在 [贡献者墙](/zh-cn/contributors) 认识正在推动 IvorySQL 前进的社区贡献者。 + ## 对话 Hey everybody! 中国IvorySQL开源数据库社区已经成立啦!如果想要加入IvorySQL中国技术交流群,可添加IvorySQL小助理微信:IvorySQL_official diff --git a/i18n/zh-CN/docusaurus-theme-classic/navbar.json b/i18n/zh-CN/docusaurus-theme-classic/navbar.json index c0b28d4..2c91374 100644 --- a/i18n/zh-CN/docusaurus-theme-classic/navbar.json +++ b/i18n/zh-CN/docusaurus-theme-classic/navbar.json @@ -15,6 +15,10 @@ "message": "开发者", "description": "Navbar item with label Developers" }, + "item.label.Contributors": { + "message": "贡献者", + "description": "Navbar item with label Contributors" + }, "item.label.Download": { "message": "下载", "description": "Navbar item with label Download" @@ -96,4 +100,3 @@ "description": "Navbar item with label Ecological Cooperation" } } - diff --git a/src/data/contributors.js b/src/data/contributors.js new file mode 100644 index 0000000..0a42c0d --- /dev/null +++ b/src/data/contributors.js @@ -0,0 +1,247 @@ +const CONTRIBUTORS_BY_YEAR = { + 2026: [ + 'cedric-villemain', + 'gaoxueyu', + 'grant-zhou', + 'jiaoshuntian', + 'leiyanliang', + 'liangxiangyu', + 'liuxiaohui', + 'liyuan', + 'oreo-yang', + 'panzhenhao', + 'pierre-forstmann', + 'rophy', + 'shizhuoyan', + 'steven-niu', + 'suige', + 'taozheng', + 'xiaoyu509', + 'xinjie-lyu', + 'yasir-hussain-shah', + 'zhangzhe', + 'zhaofawei', + ], + 2025: [ + 'Alex-Guo', + 'Amberwww1', + 'Bei-Fu', + 'Carlos-Chong', + 'Cary-Huang', + 'Cedric-Villemain', + 'Dapeng-Wang', + 'Denis-Lussier', + 'Fawei-Zhao', + 'Gavin-LYU', + 'Ge-Sui', + 'Grant-Zhou', + 'Hulin-Ji', + 'Imran-Zaheer', + 'JiaoShuntian', + 'Kang-Wang', + 'Lily-Wang', + 'Martin-Gerhardy', + 'Mingran-Feng', + 'Oreo-Yang', + 'Pedro-Lopez', + 'RRRRhl', + 'Renli-Zou', + 'Rophy-Tsai', + 'Ruike-Sun', + 'Ruohang-Feng', + 'Shaolin-Chu', + 'Shawn-Yan', + 'Shoubo-Wang', + 'Shuisen-Tong', + 'Steven-Niu', + 'Xiangyu-Liang', + 'Xiaohui-Liu', + 'Xueyu-Gao', + 'Yanliang-Lei', + 'Yasir-Hussain-Shah', + 'Yuan-Li', + 'Zhe-Zhang', + 'Zheng-Tao', + 'Zhenhao-Pan', + 'Zhibin-Wang', + 'Zhuoyan-Shi', + 'caffiendo', + 'ccwxl', + 'flyingbeecd', + 'huchangqiqi', + 'jerome-peng', + 'luss', + 'omstack', + 'otegami', + 'shangwei007', + 'shlei6067', + 'sjw1933', + 'tiankongbuqi', + 'xuexiaoganghs', + 'yangchunwanwusheng', + ], +}; + +const CONTRIBUTOR_ALIASES = { + gaoxueyu: 'xueyugao', + leiyanliang: 'yanlianglei', + liangxiangyu: 'xiangyuliang', + liuxiaohui: 'xiaohuiliu', + liyuan: 'yuanli', + panzhenhao: 'zhenhaopan', + rophy: 'rophytsai', + shizhuoyan: 'zhuoyanshi', + suige: 'gesui', + taozheng: 'zhengtao', + zhaofawei: 'faweizhao', + zhangzhe: 'zhezhang', +}; + +const CONTRIBUTOR_OVERRIDES = { + carloschong: { github: 'Carlos-Chong200' }, + caryhuang: { github: 'caryhuang' }, + cedricvillemain: { name: 'Cedric Villemain', github: 'c2main' }, + faweizhao: { + name: 'Fawei Zhao', + github: 'faweizhao26', + avatarSrc: '/img/contributors/fawei-zhao.jpeg', + }, + gavinlyu: { name: 'Gavin LYU' }, + gesui: { name: 'Ge Sui', github: 'suige' }, + grantzhou: { github: 'grantzhou' }, + imranzaheer: { github: 'imranzaheer612' }, + jiaoshuntian: { name: 'Jiao Shuntian', github: 'jiaoshuntian' }, + martingerhardy: { github: 'mgerhardy' }, + oreoyang: { name: 'Oreo Yang', github: 'OreoYang' }, + pierreforstmann: { name: 'Pierre Forstmann', github: 'pierreforstmann' }, + renlizou: { github: 'zourenli' }, + rophytsai: { name: 'Rophy Tsai', github: 'rophy' }, + rrrrhl: { github: 'RRRRhl' }, + ruohangfeng: { github: 'Vonng' }, + shaolinchu: { github: 'shaolinchu' }, + shawnyan: { github: 'shawn0915' }, + stevenniu: { name: 'Steven Niu', github: 'bigplaice' }, + xiaohuiliu: { name: 'Xiaohui Liu', github: 'hs-liuxh' }, + xiangyuliang: { name: 'Xiangyu Liang', github: 'balinorLiang' }, + xinjielyu: { name: 'Xinjie Lyu' }, + xueyugao: { name: 'Xueyu Gao', github: 'gaoxueyu' }, + yanlianglei: { name: 'Yanliang Lei', github: 'msdnchina' }, + yasirhussainshah: { name: 'Yasir Hussain Shah', github: 'yasir-hussain-shah' }, + yuanli: { name: 'Yuan Li', github: 'yuanyl630' }, + zhezhang: { name: 'Zhe Zhang', github: 'zhangzhe' }, + zhengtao: { name: 'Zheng Tao', github: 'NotHimmel' }, + zhenhaopan: { name: 'Zhenhao Pan', github: 'panzhenhao' }, + zhibinwang: { github: 'killerwzb' }, + zhuoyanshi: { name: 'Zhuoyan Shi', github: 'shizhuoyan' }, +}; + +function normalizeContributorId(value) { + return value + .normalize('NFKD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-zA-Z0-9]/g, '') + .toLowerCase(); +} + +function looksLikeGithubHandle(value) { + return /^[a-z0-9-]+$/.test(value); +} + +function slugBeautyScore(value) { + let score = 0; + if (/[-_]/.test(value)) { + score += 2; + } + if (/[A-Z]/.test(value.slice(1))) { + score += 2; + } + if (!looksLikeGithubHandle(value)) { + score += 1; + } + return score; +} + +function humanizeContributorName(value) { + if (/^[a-z0-9]+$/.test(value)) { + return `@${value}`; + } + + const words = value + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/[-_]+/g, ' ') + .trim() + .split(/\s+/); + + return words + .map((word) => { + if (!word) { + return word; + } + if (word === word.toUpperCase()) { + return word; + } + if (/^[a-z0-9]+$/.test(word)) { + return `${word.charAt(0).toUpperCase()}${word.slice(1)}`; + } + return word; + }) + .join(' '); +} + +const contributorMap = new Map(); + +Object.entries(CONTRIBUTORS_BY_YEAR).forEach(([year, slugs]) => { + slugs.forEach((slug) => { + const normalizedId = normalizeContributorId(slug); + const canonicalId = CONTRIBUTOR_ALIASES[normalizedId] || normalizedId; + const existing = contributorMap.get(canonicalId) || { + id: canonicalId, + displaySlug: slug, + github: null, + years: new Set(), + slugScore: slugBeautyScore(slug), + }; + + const nextScore = slugBeautyScore(slug); + if (nextScore > existing.slugScore) { + existing.displaySlug = slug; + existing.slugScore = nextScore; + } + + if (looksLikeGithubHandle(slug) && !existing.github) { + existing.github = slug; + } + + existing.years.add(Number(year)); + contributorMap.set(canonicalId, existing); + }); +}); + +export const contributors = Array.from(contributorMap.values()) + .map((item) => { + const override = CONTRIBUTOR_OVERRIDES[item.id] || {}; + const years = Array.from(item.years).sort((a, b) => b - a); + + return { + id: item.id, + name: override.name || humanizeContributorName(item.displaySlug), + github: override.github || item.github || null, + avatarSrc: override.avatarSrc || null, + avatarMode: override.avatarMode || null, + years, + latestYear: years[0], + }; + }) + .sort((left, right) => { + if (right.latestYear !== left.latestYear) { + return right.latestYear - left.latestYear; + } + + const leftName = left.name.replace(/^@/, '').toLowerCase(); + const rightName = right.name.replace(/^@/, '').toLowerCase(); + return leftName.localeCompare(rightName); + }); + +export const contributorYears = Object.keys(CONTRIBUTORS_BY_YEAR) + .map(Number) + .sort((a, b) => b - a); diff --git a/src/pages/community-page.md b/src/pages/community-page.md index f1b9a78..50b1374 100644 --- a/src/pages/community-page.md +++ b/src/pages/community-page.md @@ -19,6 +19,9 @@ Read our [**contribution guidelines**](https://github.com/IvorySQL/IvorySQL/blob - [Report a Bug](https://github.com/IvorySQL/IvorySQL/issues/new/choose) - [Submit a pull request](https://github.com/IvorySQL/IvorySQL/pulls) +## Contributors Wall +Meet the people helping IvorySQL grow on our [Contributors Wall](/contributors). + ## Conversations Participate in IvorySQL community discussions through the following channels: diff --git a/src/pages/contributors.js b/src/pages/contributors.js new file mode 100644 index 0000000..ad170f4 --- /dev/null +++ b/src/pages/contributors.js @@ -0,0 +1,247 @@ +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import Layout from '@theme/Layout'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import React, { useDeferredValue, useMemo, useState } from 'react'; +import { contributorYears, contributors } from '../data/contributors'; +import styles from './contributors.module.css'; + +const CERTIFICATE_REPO_URL = 'https://github.com/IvorySQL/community'; + +const COPY = { + en: { + pageTitle: 'Contributors Wall', + pageDescription: + 'A wall highlighting the people who contribute to IvorySQL.', + heroTitle: 'IvorySQL Contributors Wall', + heroDescription: + 'This page highlights contributors to IvorySQL. They help move the project forward by submitting code, opening issues, fixing bugs, improving documentation, participating in testing, and growing the community.', + heroMeta: { + contributors: 'contributors', + years: 'included', + category: 'Category: Contributor', + }, + filters: { + all: 'All Contributors', + searchPlaceholder: 'Search by name or GitHub handle', + }, + sectionTitle: 'Community Contributors', + resultLabel: 'contributors shown', + noGithub: 'Community contributor', + empty: 'No contributors matched the current filter.', + ctaText: + 'Want to appear here? Start by joining the IvorySQL contribution flow.', + certificateText: + 'Contributor certificates are available in the IvorySQL community repository.', + certificateLink: 'View or claim your contributor certificate', + ctaButton: 'View Contribution Guidelines', + }, + zh: { + pageTitle: '贡献者墙', + pageDescription: '展示 IvorySQL 贡献者的页面。', + heroTitle: 'IvorySQL 贡献者墙', + heroDescription: + '本页面展示的是 IvorySQL 的贡献者。他们通过提交代码、提交 Issue、修复 Bug、完善文档、参与测试和推广社区等方式,为 IvorySQL 的发展持续做出贡献。', + heroMeta: { + contributors: '位贡献者', + years: '收录年份', + category: '当前类别:Contributor', + }, + filters: { + all: '全部贡献者', + searchPlaceholder: '按姓名或 GitHub handle 搜索', + }, + sectionTitle: '社区贡献者', + resultLabel: '位贡献者', + noGithub: '社区贡献者', + empty: '当前筛选条件下没有匹配的贡献者。', + ctaText: + '也想出现在这里?从参与 IvorySQL 社区贡献开始。', + certificateText: + '贡献者证书已上传到 IvorySQL community 仓库,可前往查看并领取。', + certificateLink: '查看并领取贡献者证书', + ctaButton: '查看贡献指南', + }, +}; + +function getToneClass(id) { + const seed = id.split('').reduce((sum, char) => sum + char.charCodeAt(0), 0); + return styles[`tone${seed % 6}`]; +} + +function getMonogram(name) { + const cleaned = name.replace(/^@/, ''); + const parts = cleaned.split(/[\s-]+/).filter(Boolean); + if (parts.length >= 2) { + return `${parts[0][0]}${parts[1][0]}`.toUpperCase(); + } + return cleaned.slice(0, 2).toUpperCase(); +} + +function ContributorCard({ contributor, copy }) { + const toneClass = getToneClass(contributor.id); + const githubUrl = contributor.github ? `https://github.com/${contributor.github}` : null; + const localAvatarUrl = contributor.avatarSrc ? useBaseUrl(contributor.avatarSrc) : null; + const showGithubAvatar = !localAvatarUrl && githubUrl && contributor.avatarMode !== 'monogram'; + const avatarSrc = localAvatarUrl || (showGithubAvatar + ? `https://github.com/${contributor.github}.png?size=240` + : null); + const handleText = githubUrl ? `@${contributor.github}` : ''; + const yearsLabel = contributor.years.join(' · '); + + return ( +
+
+
+ {avatarSrc ? ( + {contributor.name} + ) : ( + {getMonogram(contributor.name)} + )} +
+
+ {githubUrl ? ( + +

{contributor.name}

+
+ ) : ( +

{contributor.name}

+ )} +

{handleText}

+
+
+ +

{yearsLabel}

+
+ ); +} + +export default function ContributorsPage() { + const { i18n } = useDocusaurusContext(); + const isZh = i18n.currentLocale.toLowerCase().startsWith('zh'); + const copy = isZh ? COPY.zh : COPY.en; + const [selectedYear, setSelectedYear] = useState('all'); + const [query, setQuery] = useState(''); + const deferredQuery = useDeferredValue(query.trim().toLowerCase()); + const includedYears = `${contributorYears[contributorYears.length - 1]}-${contributorYears[0]}`; + + const filterOptions = useMemo( + () => [ + { key: 'all', label: copy.filters.all }, + ...contributorYears.map((year) => ({ key: String(year), label: String(year) })), + ], + [copy.filters.all], + ); + + const filteredContributors = useMemo(() => { + return contributors.filter((item) => { + const matchesYear = + selectedYear === 'all' || item.years.includes(Number(selectedYear)); + const matchesQuery = + !deferredQuery || + item.name.toLowerCase().includes(deferredQuery) || + (item.github && item.github.toLowerCase().includes(deferredQuery)); + + return matchesYear && matchesQuery; + }); + }, [deferredQuery, selectedYear]); + + return ( + +
+
+
+
+

{copy.heroTitle}

+

{copy.heroDescription}

+
+ {contributors.length} {copy.heroMeta.contributors} + {includedYears} {copy.heroMeta.years} + {copy.heroMeta.category} +
+
+
+ +
+
+ {filterOptions.map((option) => ( + + ))} +
+ +
+ setQuery(event.target.value)} + placeholder={copy.filters.searchPlaceholder} + aria-label={copy.filters.searchPlaceholder} + /> +
+
+ +
+
+

{copy.sectionTitle}

+

+ {filteredContributors.length} {copy.resultLabel} +

+
+ + {filteredContributors.length ? ( +
+ {filteredContributors.map((contributor) => ( + + ))} +
+ ) : ( +
{copy.empty}
+ )} +
+ +
+
+

{copy.ctaText}

+

+ {copy.certificateText}{' '} + + {copy.certificateLink} + +

+
+ + {copy.ctaButton} + +
+
+
+
+ ); +} diff --git a/src/pages/contributors.module.css b/src/pages/contributors.module.css new file mode 100644 index 0000000..be29d07 --- /dev/null +++ b/src/pages/contributors.module.css @@ -0,0 +1,371 @@ +.page { + --contributors-accent: #2f74ff; + --contributors-accent-soft: rgba(47, 116, 255, 0.12); + --contributors-ink: #111827; + --contributors-muted: #64748b; + --contributors-line: #e5e9f2; + --contributors-panel: rgba(255, 255, 255, 0.92); + --contributors-panel-strong: rgba(248, 250, 255, 0.98); + background: + radial-gradient(circle at top right, rgba(47, 116, 255, 0.1), transparent 24%), + radial-gradient(circle at left 18%, rgba(116, 157, 255, 0.08), transparent 20%), + linear-gradient(180deg, #f4f8ff 0%, #ffffff 24%, #f6f9ff 100%); + min-height: 100vh; +} + +.hero { + position: relative; + overflow: hidden; + padding: 40px; + margin: 36px 0 26px; + border-radius: 30px; + border: 1px solid var(--contributors-line); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(246, 249, 255, 0.96) 100%); + box-shadow: 0 24px 60px rgba(30, 41, 59, 0.07); +} + +.hero::before { + content: ''; + position: absolute; + inset: auto -96px -132px auto; + width: 260px; + height: 260px; + border-radius: 50%; + background: radial-gradient(circle, rgba(47, 116, 255, 0.12) 0%, rgba(47, 116, 255, 0.03) 46%, transparent 72%); +} + +.hero::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(90deg, rgba(47, 116, 255, 0.03) 0%, transparent 40%); + pointer-events: none; +} + +.heroContent { + position: relative; + z-index: 1; +} + +.heroTitle { + margin: 0 0 16px; + font-size: clamp(2.2rem, 5vw, 3.6rem); + line-height: 1.08; + color: var(--contributors-ink); +} + +.heroDescription { + max-width: 920px; + margin: 0; + font-size: 1.04rem; + line-height: 1.85; + color: var(--contributors-muted); +} + +.heroMeta { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 18px; + color: var(--contributors-muted); + font-size: 0.92rem; +} + +.heroMeta span { + display: inline-flex; + align-items: center; + padding: 8px 12px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(47, 116, 255, 0.1); +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 14px; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; + padding: 14px 0 10px; +} + +.chipGroup { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.chip { + border: 1px solid rgba(17, 24, 39, 0.08); + background: rgba(255, 255, 255, 0.92); + color: var(--contributors-muted); + border-radius: 999px; + padding: 9px 14px; + font-size: 0.92rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.chip:hover { + border-color: rgba(47, 116, 255, 0.35); + color: var(--contributors-accent); + background: #fff; +} + +.chipActive { + border-color: transparent; + background: var(--contributors-accent); + color: #fff; + box-shadow: 0 12px 28px rgba(47, 116, 255, 0.2); +} + +.searchWrap { + flex: 1; + min-width: min(100%, 280px); +} + +.searchInput { + width: 100%; + padding: 11px 15px; + border-radius: 14px; + border: 1px solid rgba(47, 116, 255, 0.12); + background: rgba(255, 255, 255, 0.96); + font-size: 0.96rem; + color: var(--contributors-ink); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); +} + +.searchInput:focus { + outline: none; + border-color: rgba(47, 116, 255, 0.45); + box-shadow: 0 0 0 4px rgba(47, 116, 255, 0.1); +} + +.section { + padding: 6px 0 30px; +} + +.sectionHeader { + display: flex; + flex-wrap: wrap; + gap: 14px; + align-items: center; + justify-content: space-between; + margin-bottom: 18px; +} + +.sectionTitle { + margin: 0; + font-size: clamp(1.55rem, 3vw, 2rem); + color: var(--contributors-ink); +} + +.resultLabel { + margin: 0; + color: var(--contributors-muted); + font-size: 0.95rem; +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; +} + +.card { + display: flex; + flex-direction: column; + gap: 12px; + min-height: 0; + padding: 18px; + border-radius: 20px; + border: 1px solid rgba(47, 116, 255, 0.08); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 255, 0.96) 100%); + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.05); + transition: transform 0.22s ease, box-shadow 0.22s ease, border-color 0.22s ease; +} + +.card:hover { + transform: translateY(-3px); + border-color: rgba(47, 116, 255, 0.18); + box-shadow: 0 18px 34px rgba(47, 116, 255, 0.08); +} + +.cardHeader { + display: flex; + align-items: center; + gap: 14px; +} + +.avatar { + width: 58px; + height: 58px; + border-radius: 18px; + overflow: hidden; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: 700; + font-size: 1.15rem; + letter-spacing: 0.04em; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.avatarFallback { + display: flex; + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + white-space: nowrap; + line-height: 1; + font-size: 1rem; + letter-spacing: 0.08em; +} + +.tone0 { background: linear-gradient(135deg, #2f66ff 0%, #6d8cff 100%); } +.tone1 { background: linear-gradient(135deg, #ff845c 0%, #ffb36f 100%); } +.tone2 { background: linear-gradient(135deg, #161f4b 0%, #3e56b0 100%); } +.tone3 { background: linear-gradient(135deg, #00a37a 0%, #2bc3a0 100%); } +.tone4 { background: linear-gradient(135deg, #5f3dc4 0%, #8f5cf6 100%); } +.tone5 { background: linear-gradient(135deg, #935116 0%, #d78f39 100%); } + +.cardTitleBlock { + min-width: 0; +} + +.cardTitle { + margin: 0; + font-size: 1.02rem; + color: var(--contributors-ink); + line-height: 1.35; +} + +.cardTitleLink { + color: inherit; + text-decoration: none; +} + +.cardTitleLink:hover .cardTitle { + color: var(--contributors-accent); +} + +.cardHandle { + margin: 4px 0 0; + color: var(--contributors-muted); + font-size: 0.92rem; + line-height: 1.4; + min-height: 1.4em; +} + +.cardYears { + margin: 2px 0 0; + color: var(--contributors-accent); + font-size: 0.86rem; + font-weight: 700; +} + +.empty { + padding: 34px 26px; + border-radius: 24px; + border: 1px dashed rgba(47, 116, 255, 0.2); + background: rgba(255, 255, 255, 0.82); + color: var(--contributors-muted); + text-align: center; + line-height: 1.8; +} + +.cta { + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + margin: 22px 0 46px; + padding-top: 18px; + border-top: 1px solid rgba(47, 116, 255, 0.12); +} + +.ctaContent { + display: flex; + flex-direction: column; + gap: 8px; + max-width: 760px; +} + +.ctaText { + margin: 0; + color: var(--contributors-muted); + line-height: 1.8; +} + +.ctaSubtext { + margin: 0; + color: var(--contributors-muted); + line-height: 1.75; + font-size: 0.95rem; +} + +.ctaInlineLink { + color: var(--contributors-accent); + text-decoration: none; + font-weight: 600; +} + +.ctaInlineLink:hover { + color: #225fe6; + text-decoration: underline; +} + +.ctaButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 18px; + border-radius: 999px; + background: var(--contributors-accent); + color: #fff; + text-decoration: none; + font-weight: 700; + box-shadow: 0 12px 26px rgba(47, 116, 255, 0.18); +} + +.ctaButton:hover { + color: #fff; + background: #225fe6; +} + +@media (max-width: 996px) { + .hero { + padding: 28px; + border-radius: 28px; + } + + .controls { + align-items: stretch; + } + + .searchWrap { + width: 100%; + } +} + +@media (max-width: 640px) { + .heroTitle { + font-size: 2.35rem; + } + + .cta { + align-items: flex-start; + } +} diff --git a/static/img/contributors/fawei-zhao.jpeg b/static/img/contributors/fawei-zhao.jpeg new file mode 100644 index 0000000..7c29bb3 Binary files /dev/null and b/static/img/contributors/fawei-zhao.jpeg differ