Drop GameServerStats, mark GameServerLists coming soon, list planned OpenVibe domains, fix reveal overlap
Changes
BBinary files a/public/assets/img/previews/alexfrison.net.jpg and b/public/assets/img/previews/alexfrison.net.jpg differBBinary files a/public/assets/img/previews/local_home.jpg and b/public/assets/img/previews/local_home.jpg differBBinary files a/public/assets/img/previews/local_home__about.jpg and b/public/assets/img/previews/local_home__about.jpg differBBinary files a/public/assets/img/previews/local_home__contact.jpg and b/public/assets/img/previews/local_home__contact.jpg differBBinary files a/public/assets/img/previews/local_home__experience.jpg and b/public/assets/img/previews/local_home__experience.jpg differBBinary files a/public/assets/img/previews/local_home__metrics.jpg and b/public/assets/img/previews/local_home__metrics.jpg differBBinary files a/public/assets/img/previews/local_home__network.jpg and b/public/assets/img/previews/local_home__network.jpg differBBinary files a/public/assets/img/previews/local_home__skills.jpg and b/public/assets/img/previews/local_home__skills.jpg differBBinary files a/public/assets/img/previews/local_home__work.jpg and b/public/assets/img/previews/local_home__work.jpg differBBinary files a/public/assets/img/previews/local_portfolio.jpg and b/public/assets/img/previews/local_portfolio.jpg differBBinary files a/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg and b/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg differBBinary files a/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg and b/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg differBBinary files a/public/assets/img/previews/local_portfolio_gameserverlists.jpg and b/public/assets/img/previews/local_portfolio_gameserverlists.jpg differddeleted file mode 100644BBinary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and /dev/null differBBinary files a/public/assets/img/previews/local_portfolio_hartools.jpg and b/public/assets/img/previews/local_portfolio_hartools.jpg differBBinary files a/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg and b/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg differBBinary files a/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg and b/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg differBBinary files a/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg and b/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg differBBinary files a/public/assets/img/previews/local_portfolio_live-translation.jpg and b/public/assets/img/previews/local_portfolio_live-translation.jpg differBBinary files a/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg and b/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg differBBinary files a/public/assets/img/previews/local_portfolio_mp-select-mini.jpg and b/public/assets/img/previews/local_portfolio_mp-select-mini.jpg differBBinary files a/public/assets/img/previews/local_portfolio_openvibe-community.jpg and b/public/assets/img/previews/local_portfolio_openvibe-community.jpg differBBinary files a/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg and b/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg differBBinary files a/public/assets/img/previews/local_portfolio_openvibe-live.jpg and b/public/assets/img/previews/local_portfolio_openvibe-live.jpg differBBinary files a/public/assets/img/previews/local_portfolio_openvibe-media.jpg and b/public/assets/img/previews/local_portfolio_openvibe-media.jpg differBBinary files a/public/assets/img/previews/local_portfolio_openvibe-sites.jpg and b/public/assets/img/previews/local_portfolio_openvibe-sites.jpg differBBinary files a/public/assets/img/previews/local_portfolio_perf-harness.jpg and b/public/assets/img/previews/local_portfolio_perf-harness.jpg differBBinary files a/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg and b/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg differBBinary files a/public/assets/img/previews/local_portfolio_powerchat-payments.jpg and b/public/assets/img/previews/local_portfolio_powerchat-payments.jpg differBBinary files a/public/assets/img/previews/local_portfolio_pytools-workshop.jpg and b/public/assets/img/previews/local_portfolio_pytools-workshop.jpg differBBinary files a/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg and b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg differBBinary files a/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg and b/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openre.stream.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.blog.jpg differBBinary files a/public/assets/img/previews/openvibe.chat.jpg and b/public/assets/img/previews/openvibe.chat.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.codes.jpg differBBinary files a/public/assets/img/previews/openvibe.community.jpg and b/public/assets/img/previews/openvibe.community.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.coupons.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.deals.jpg differBBinary files a/public/assets/img/previews/openvibe.games.jpg and b/public/assets/img/previews/openvibe.games.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.host.jpg differBBinary files a/public/assets/img/previews/openvibe.live.jpg and b/public/assets/img/previews/openvibe.live.jpg differBBinary files a/public/assets/img/previews/openvibe.media.jpg and b/public/assets/img/previews/openvibe.media.jpg differBBinary files a/public/assets/img/previews/openvibe.network.jpg and b/public/assets/img/previews/openvibe.network.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.news.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.reviews.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.tips.jpg differBBinary files a/public/assets/img/previews/openvibe.tools.jpg and b/public/assets/img/previews/openvibe.tools.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.trade.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.vip.jpg differnnew file mode 100644BBinary files /dev/null and b/public/assets/img/previews/openvibe.wiki.jpg differ@@@ -13,6 +13,7 @@ const W = 1280, H = 800, SCALE = 0.5; // 640x400 outputconst external = [...profile.network.map((n) => n.url),++ ...(profile.networkPlanned || []).map((n) => n.url),'https://powerchat.live','https://github.com/OpenVibers','https://alexfrison.net',@@@ -5,7 +5,6 @@ import { linkify } from "../lib/linkify";const LINK_TERMS = {"OpenVibe": { href: "https://openvibe.network", external: true },"PowerChat": { href: "https://powerchat.live", external: true },-- "GameServerStats": { href: "/portfolio/gameserverstats" },};const pitchParts = linkify(profile.pitch, LINK_TERMS);const statusNowParts = linkify(profile.status.now, LINK_TERMS);@@@ -43,7 +43,7 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));<div class="pcard__top"><span class="pcard__icon"><i class={p.data.icon ?? "fa-solid fa-cube"}></i></span><span class="pcard__cat">{catLabel[p.data.category] ?? p.data.category}</span>-- {p.data.year && <span class="pcard__year mono">{p.data.year}</span>}++ {p.data.status === "coming-soon" ? <span class="pcard__soon mono"><i class="fa-solid fa-clock"></i> Coming soon</span> : p.data.year && <span class="pcard__year mono">{p.data.year}</span>}</div><h3 class="pcard__title">{p.data.title}</h3><p class="pcard__sum">{p.data.summary}</p>@@@ -108,6 +108,7 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));.cat-infra .pcard__icon { background: rgba(139, 92, 246, .14); color: var(--violet); border-color: rgba(139, 92, 246, .25); }.pcard__cat { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }.pcard__year { margin-left: auto; font-size: .7rem; color: var(--dim); }++ .pcard__soon { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }.pcard__title { margin: 0; font-size: 1.15rem; }.pcard__sum { margin: 0; font-size: .9rem; color: var(--muted); line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }.pcard__tech { list-style: none; margin: auto 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }@@@ -28,25 +28,25 @@ const groups = [key: "openvibe", tone: "a", anchor: "network", icon: "fa-solid fa-diagram-project",title: "The OpenVibe network", blurb: "Seven live services behind one sign-in, all built and run by me since 2026.",count: profile.network.length, allHref: "/portfolio/?cat=openvibe,infra", allLabel: "All OpenVibe case notes",-- items: [] as { href: string; external: boolean; image?: string | null; icon: string; title: string; sub: string; text: string; meta: string }[],++ items: [] as { href: string; external: boolean; image?: string | null; icon: string; title: string; sub: string; text: string; meta: string; soon?: boolean }[],},{key: "powerchat", tone: "b", icon: "fa-solid fa-bolt",title: "PowerChat", blurb: "Contract platform work for a creator-monetization product: payments and the developer API.",count: countCats(["powerchat"]), allHref: "/portfolio/?cat=powerchat", allLabel: "All PowerChat work",-- items: byCats(["powerchat"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "" })),++ items: byCats(["powerchat"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "", soon: p.data.status === "coming-soon" })),},{key: "ai", tone: "c", icon: "fa-solid fa-robot",title: "AI, data & automation", blurb: "Agents, analytics and tooling that did real work at Amazon and inside OpenVibe.",count: countCats(["ai", "amazon"]), allHref: "/portfolio/?cat=ai,amazon", allLabel: "All AI & data work",-- items: byCats(["ai", "amazon"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "" })),++ items: byCats(["ai", "amazon"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "", soon: p.data.status === "coming-soon" })),},{key: "more", tone: "d", icon: "fa-solid fa-shapes",title: "Games, tools & side projects", blurb: "The builds that keep the tools sharp: a multiplayer game, desktop apps, hardware and utilities.",count: countCats(["game", "tools", "python", "hardware", "web"]), allHref: "/portfolio/", allLabel: "The whole archive",-- items: byCats(["game", "tools", "python", "hardware", "web"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "" })),++ items: byCats(["game", "tools", "python", "hardware", "web"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "", soon: p.data.status === "coming-soon" })),},];---@@@ -63,7 +63,7 @@ const groups = [<div class="wk">{groups.map((g, gi) => (-- <section class={`wk-group wk-group--${g.tone}`} id={g.anchor} aria-labelledby={`wk-${g.key}`} data-reveal style={`--i:${gi}`}>++ <section class={`wk-group wk-group--${g.tone}`} id={g.anchor} aria-labelledby={`wk-${g.key}`} style={`--i:${gi}`}><header class="wk-group__head"><span class="wk-group__ico"><i class={g.icon} aria-hidden="true"></i></span><div class="wk-group__txt">@@@ -73,21 +73,31 @@ const groups = [<a class="wk-group__all" href={g.allHref}><i class="fa-solid fa-grip" aria-hidden="true"></i> <span>{g.allLabel}</span> <i class="fa-solid fa-arrow-right wk-group__arrow" aria-hidden="true"></i></a></header>{g.key === "openvibe" && <OpenVibeBento />}++ {g.key === "openvibe" && (++ <div class="wk-planned" aria-label="Planned OpenVibe sites">++ <span class="wk-planned__label"><i class="fa-solid fa-clock" aria-hidden="true"></i> Coming soon</span>++ <ul class="wk-planned__list">++ {profile.networkPlanned.map((n) => (++ <li><a href={n.url} target="_blank" rel="noopener" title={`${n.name}: ${n.role} (placeholder page)`}>{n.name.replace("OpenVibe.", ".").replace("OpenRe.", "OpenRe.")}<small>{n.role}</small></a></li>++ ))}++ </ul>++ </div>++ )}<ul class="wk-grid" data-stagger hidden={g.items.length === 0}>{g.items.map((it) => (-- <li class={`wk-card ${it.image ? "wk-card--img" : ""}`}>++ <li class={`wk-card ${it.image ? "wk-card--img" : ""} ${it.soon ? "wk-card--soon" : ""}`}><a href={it.href} class="wk-card__link" target={it.external ? "_blank" : undefined} rel={it.external ? "noopener" : undefined}>{it.image && <span class="wk-card__thumb" aria-hidden="true"><img src={it.image} alt="" loading="lazy" width="640" height="240" /></span>}<span class="wk-card__body"><span class="wk-card__top"><span class="wk-card__ico"><i class={it.icon} aria-hidden="true"></i></span>{!it.image && <span class="wk-card__sub">{it.sub}</span>}-- {it.meta && <span class="wk-card__meta mono">{it.meta}</span>}++ {it.soon ? <span class="wk-card__soon mono"><i class="fa-solid fa-clock" aria-hidden="true"></i> Coming soon</span> : it.meta && <span class="wk-card__meta mono">{it.meta}</span>}</span><span class="wk-card__title">{it.title}</span>{it.image && <span class="wk-card__role">{it.sub}</span>}<span class="wk-card__text">{it.text}</span>-- <span class="wk-card__go"><span>{it.external ? "Open site" : "Case notes"}</span> <i class={it.external ? "fa-solid fa-arrow-up-right-from-square" : "fa-solid fa-arrow-right"} aria-hidden="true"></i></span>++ <span class="wk-card__go"><span>{it.soon ? "Preview the plan" : it.external ? "Open site" : "Case notes"}</span> <i class={it.external ? "fa-solid fa-arrow-up-right-from-square" : "fa-solid fa-arrow-right"} aria-hidden="true"></i></span></span></a></li>@@@ -150,6 +160,16 @@ const groups = [.wk-card__go i { transition: transform .3s var(--ease); font-size: .7rem; }.wk-card__link:hover .wk-card__go i { transform: translateX(3px); }.mono { font-family: var(--font-mono); }++ .wk-card__soon { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); white-space: nowrap; }++ .wk-card--soon .wk-card__link { border-style: dashed; }++ /* Planned domains: every one already has a truthful "coming soon" page, so the chips link there. */++ .wk-planned { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin-top: 14px; padding: 12px 14px; border-radius: 14px; border: 1px dashed var(--border-strong); background: var(--surface); }++ .wk-planned__label { display: inline-flex; align-items: center; gap: 7px; font: 600 .68rem var(--font-mono); letter-spacing: .14em; text-transform: uppercase; color: var(--accent-3); }++ .wk-planned__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }++ .wk-planned__list a { display: inline-flex; align-items: baseline; gap: 6px; padding: 5px 10px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface-2); color: var(--fg); font: 600 .74rem var(--font-body); text-decoration: none; transition: border-color .25s var(--ease), transform .25s var(--ease); }++ .wk-planned__list a small { font: 500 .62rem var(--font-mono); color: var(--dim); letter-spacing: .04em; }++ .wk-planned__list a:hover { text-decoration: none; border-color: rgba(var(--accent-3-rgb), .55); transform: translateY(-1px); }++ @media (max-width: 560px) { .wk-planned__list a small { display: none; } }.work__more { margin-top: clamp(22px, 3vw, 34px); display: flex; justify-content: center; }.work__all-btn {@@@ -39,6 +39,7 @@ const projects = defineCollection({video: z.string().url().optional(),}).default({}),featured: z.boolean().default(false),++ status: z.enum(['live', 'coming-soon', 'archived']).default('live'),order: z.number().default(0),}),});@@@ -8,7 +8,7 @@"bullets": ["Built analytics platforms tracking 200,000+ live game servers: ingestion, search, ranking, dashboards and licensed partner APIs. Ranked first on Google for several major titles.","Self-built and collocated 1U and 2U servers in datacenters; designed for scale and DDoS resilience and ran the whole LLC, from licenses and taxes to partner relations.",-- "Rebuilt the idea in 2026 as GameServerLists: a 14-service, game-agnostic server-discovery platform with multi-region probes, time-weighted ranking, an anti-fraud integrity engine and a tiered public API."++ "Rebuilding the idea in 2026 as GameServerLists (launching soon): a 14-service, game-agnostic server-discovery platform with multi-region probes, time-weighted ranking, an anti-fraud integrity engine and a tiered public API."],"stack": ["Node.js",@@@ -21,4 +21,4 @@"NGINX","Linux"]--}\\ No newline at end of file++}@@@ -1,10 +1,11 @@---title: "GameServerLists"category: "web"--summary: "2026 rebuild of the server-tracking idea as a 14-service, game-agnostic discovery platform: multi-region probes, time-weighted ranking, anti-fraud integrity engine and a tiered public API."++summary: "Launching soon: a 14-service, game-agnostic server-discovery platform with multi-region probes, time-weighted ranking, an anti-fraud integrity engine and a tiered public API."tech: ["TypeScript", "pnpm monorepo", "TimescaleDB", "Redis", "A2S / Minecraft SLP / EOS", "WebSockets", "Provider-abstracted AI"]role: "Architect · Sole Engineer"--year: "2026"++year: "Coming soon"++status: "coming-soon"icon: "fa-solid fa-list-check"order: 13---ddeleted file mode 100644@@@ -1,15 +0,0 @@--title: "GameServerStats"--category: "web"--summary: "Analytics platform tracking 200,000+ live game servers — search, ranking, dashboards, and partner APIs."--tech: ["Node.js", "PHP", "MongoDB", "Redis", "NGINX", "Linux"]--role: "Founder / CEO / Sole Engineer"--year: "2015–2026"--icon: "fa-solid fa-server"--order: 30----A decade of analytics for the multiplayer-game-server world. Custom--ingestion fetches and computes stats on hundreds of thousands of servers,--serves the #1 Google result for several major games, and licensed to--third-party sites under written agreement.@@@ -27,6 +27,54 @@"img": "/assets/img/previews/openvibe.chat.jpg","label": "openvibe.chat"},++ "openre.stream": {++ "img": "/assets/img/previews/openre.stream.jpg",++ "label": "openre.stream"++ },++ "openvibe.tips": {++ "img": "/assets/img/previews/openvibe.tips.jpg",++ "label": "openvibe.tips"++ },++ "openvibe.vip": {++ "img": "/assets/img/previews/openvibe.vip.jpg",++ "label": "openvibe.vip"++ },++ "openvibe.codes": {++ "img": "/assets/img/previews/openvibe.codes.jpg",++ "label": "openvibe.codes"++ },++ "openvibe.host": {++ "img": "/assets/img/previews/openvibe.host.jpg",++ "label": "openvibe.host"++ },++ "openvibe.wiki": {++ "img": "/assets/img/previews/openvibe.wiki.jpg",++ "label": "openvibe.wiki"++ },++ "openvibe.blog": {++ "img": "/assets/img/previews/openvibe.blog.jpg",++ "label": "openvibe.blog"++ },++ "openvibe.news": {++ "img": "/assets/img/previews/openvibe.news.jpg",++ "label": "openvibe.news"++ },++ "openvibe.reviews": {++ "img": "/assets/img/previews/openvibe.reviews.jpg",++ "label": "openvibe.reviews"++ },++ "openvibe.deals": {++ "img": "/assets/img/previews/openvibe.deals.jpg",++ "label": "openvibe.deals"++ },++ "openvibe.coupons": {++ "img": "/assets/img/previews/openvibe.coupons.jpg",++ "label": "openvibe.coupons"++ },++ "openvibe.trade": {++ "img": "/assets/img/previews/openvibe.trade.jpg",++ "label": "openvibe.trade"++ },"powerchat.live": {"img": "/assets/img/previews/powerchat.live.jpg","label": "powerchat.live"@@@ -63,10 +111,6 @@"img": "/assets/img/previews/local_portfolio_gameserverlists.jpg","label": "alexfrison.net/portfolio/gameserverlists"},-- "path:/portfolio/gameserverstats": {-- "img": "/assets/img/previews/local_portfolio_gameserverstats.jpg",-- "label": "alexfrison.net/portfolio/gameserverstats"-- },"path:/portfolio/hartools": {"img": "/assets/img/previews/local_portfolio_hartools.jpg","label": "alexfrison.net/portfolio/hartools"@@@ -70,6 +70,21 @@{ "key": "community", "name": "OpenVibe.Community", "url": "https://openvibe.community", "role": "Pastes & posts", "blurb": "Server-rendered paste hub with forks, search, feeds and silent SSO, proxied to Live so identity stays consistent.", "stat": "No DB of its own", "image": "/assets/img/sites/community.webp" },{ "key": "sites", "name": "OpenVibe.Sites", "url": "https://openvibe.chat", "role": "13 front doors", "blurb": "One catalog and one template generate the pages and nginx configs for openvibe.chat, .codes, .wiki, .news and more.", "stat": "13 domains · 1 build", "image": null }],++ "networkPlanned": [++ { "name": "OpenRe.Stream", "url": "https://openre.stream", "role": "Restreaming" },++ { "name": "OpenVibe.Chat", "url": "https://openvibe.chat", "role": "Chat & calls" },++ { "name": "OpenVibe.Tips", "url": "https://openvibe.tips", "role": "Creator tips" },++ { "name": "OpenVibe.VIP", "url": "https://openvibe.vip", "role": "Memberships" },++ { "name": "OpenVibe.Codes", "url": "https://openvibe.codes", "role": "Developer portal" },++ { "name": "OpenVibe.Host", "url": "https://openvibe.host", "role": "Hosting" },++ { "name": "OpenVibe.Wiki", "url": "https://openvibe.wiki", "role": "Wiki" },++ { "name": "OpenVibe.Blog", "url": "https://openvibe.blog", "role": "Blogs" },++ { "name": "OpenVibe.News", "url": "https://openvibe.news", "role": "News" },++ { "name": "OpenVibe.Reviews", "url": "https://openvibe.reviews", "role": "Reviews" },++ { "name": "OpenVibe.Deals", "url": "https://openvibe.deals", "role": "Deals" },++ { "name": "OpenVibe.Coupons", "url": "https://openvibe.coupons", "role": "Coupons" },++ { "name": "OpenVibe.Trade", "url": "https://openvibe.trade", "role": "Markets" }++ ],"education": "Snohomish High School, WA. AP Statistics, Calculus, English, Creative Writing and Spanish. Self-taught engineer since age 12: systems, infrastructure, graphics, hardware and whatever the problem needs.","capabilities": ["websites", "platforms", "APIs", "dashboards", "AI features", "automations", "identity systems", "payment systems", "real-time chat", "streaming infra", "whatever you need"]}@@@ -248,12 +248,14 @@ const imageUrl = new URL(image, site).toString();const el = en.target as HTMLElement;const kind = el.dataset.reveal || 'up';const delay = Number(el.dataset.delay || 0);-- const from: Keyframe = kind === 'left' ? { opacity: 0, transform: 'translateX(-30px)' }-- : kind === 'right' ? { opacity: 0, transform: 'translateX(30px)' }-- : kind === 'scale' ? { opacity: 0, transform: 'scale(.94)' }-- : { opacity: 0, transform: 'translateY(26px)' };++ // Offsets stay small (and never accumulate on nested [data-reveal] ancestors): larger ones++ // made cards visibly overlap the next card mid-animation when someone scrolls fast.++ const from: Keyframe = kind === 'left' ? { opacity: 0, transform: 'translateX(-12px)' }++ : kind === 'right' ? { opacity: 0, transform: 'translateX(12px)' }++ : kind === 'scale' ? { opacity: 0, transform: 'scale(.975)' }++ : { opacity: 0, transform: 'translateY(8px) scale(.99)' };el.style.opacity = '1'; el.style.transform = 'none';-- el.animate([from, { opacity: 1, transform: 'none' }], { duration: 750, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });++ el.animate([from, { opacity: 1, transform: 'none' }], { duration: 600, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });io.unobserve(el);}}, { rootMargin: '0px 0px -8% 0px', threshold: 0.06 });@@@ -266,8 +268,8 @@ const imageUrl = new URL(image, site).toString();const kids = Array.from((en.target as HTMLElement).children) as HTMLElement[];kids.forEach((k, i) => {k.style.opacity = '1';-- k.animate([{ opacity: 0, transform: 'translateY(22px)' }, { opacity: 1, transform: 'none' }],-- { duration: 650, delay: i * 70, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });++ k.animate([{ opacity: 0, transform: 'translateY(8px) scale(.99)' }, { opacity: 1, transform: 'none' }],++ { duration: 550, delay: Math.min(i, 8) * 55, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });});sio.unobserve(en.target);}@@@ -26,7 +26,7 @@ const catLabel: Record<string, string> = {<header class="pp__head" data-reveal><div class="pp__meta-top"><span class="chip">{catLabel[d.category] ?? d.category}</span>-- {d.year && <span class="mono pp__year">{d.year}</span>}++ {d.status === "coming-soon" ? <span class="mono pp__soon"><i class="fa-solid fa-clock"></i> Coming soon</span> : d.year && <span class="mono pp__year">{d.year}</span>}</div><h1 class="pp__title">{d.title}</h1><p class="pp__lead">{d.summary}</p>@@@ -82,6 +82,7 @@ const catLabel: Record<string, string> = {.pp__back:hover { color: var(--fg); text-decoration: none; }.pp__meta-top { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }.pp__year { color: var(--dim); font-size: .8rem; }++ .pp__soon { display: inline-flex; align-items: center; gap: 6px; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }.pp__title { font-size: clamp(2.2rem, 5.5vw, 4rem); margin: 0 0 .3em; }.pp__lead { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--fg); max-width: 62ch; line-height: 1.6; }.pp__meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin: 1.4rem 0; }@@@ -12,7 +12,6 @@ const projects = (await getCollection("projects")).filter((p) => p.data.featuredconst LINK_TERMS = {"OpenVibe": { href: "https://openvibe.network", external: true },"PowerChat": { href: "https://powerchat.live", external: true },-- "GameServerStats": { href: "/portfolio/gameserverstats" },};const pitchParts = linkify(profile.pitch, LINK_TERMS);---
Binary files a/public/assets/img/previews/alexfrison.net.jpg and b/public/assets/img/previews/alexfrison.net.jpg differ
Binary files a/public/assets/img/previews/alexfrison.net.jpg and b/public/assets/img/previews/alexfrison.net.jpg differ
Binary files a/public/assets/img/previews/local_home.jpg and b/public/assets/img/previews/local_home.jpg differ
Binary files a/public/assets/img/previews/local_home.jpg and b/public/assets/img/previews/local_home.jpg differ
Binary files a/public/assets/img/previews/local_home__about.jpg and b/public/assets/img/previews/local_home__about.jpg differ
Binary files a/public/assets/img/previews/local_home__about.jpg and b/public/assets/img/previews/local_home__about.jpg differ
Binary files a/public/assets/img/previews/local_home__contact.jpg and b/public/assets/img/previews/local_home__contact.jpg differ
Binary files a/public/assets/img/previews/local_home__contact.jpg and b/public/assets/img/previews/local_home__contact.jpg differ
Binary files a/public/assets/img/previews/local_home__experience.jpg and b/public/assets/img/previews/local_home__experience.jpg differ
Binary files a/public/assets/img/previews/local_home__experience.jpg and b/public/assets/img/previews/local_home__experience.jpg differ
Binary files a/public/assets/img/previews/local_home__metrics.jpg and b/public/assets/img/previews/local_home__metrics.jpg differ
Binary files a/public/assets/img/previews/local_home__metrics.jpg and b/public/assets/img/previews/local_home__metrics.jpg differ
Binary files a/public/assets/img/previews/local_home__network.jpg and b/public/assets/img/previews/local_home__network.jpg differ
Binary files a/public/assets/img/previews/local_home__network.jpg and b/public/assets/img/previews/local_home__network.jpg differ
Binary files a/public/assets/img/previews/local_home__skills.jpg and b/public/assets/img/previews/local_home__skills.jpg differ
Binary files a/public/assets/img/previews/local_home__skills.jpg and b/public/assets/img/previews/local_home__skills.jpg differ
Binary files a/public/assets/img/previews/local_home__work.jpg and b/public/assets/img/previews/local_home__work.jpg differ
Binary files a/public/assets/img/previews/local_home__work.jpg and b/public/assets/img/previews/local_home__work.jpg differ
Binary files a/public/assets/img/previews/local_portfolio.jpg and b/public/assets/img/previews/local_portfolio.jpg differ
Binary files a/public/assets/img/previews/local_portfolio.jpg and b/public/assets/img/previews/local_portfolio.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg and b/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg and b/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg and b/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg and b/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_gameserverlists.jpg and b/public/assets/img/previews/local_portfolio_gameserverlists.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_gameserverlists.jpg and b/public/assets/img/previews/local_portfolio_gameserverlists.jpg differ
deleted file mode 100644
deleted file mode 100644
Binary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and /dev/null differ
Binary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and /dev/null differ
Binary files a/public/assets/img/previews/local_portfolio_hartools.jpg and b/public/assets/img/previews/local_portfolio_hartools.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_hartools.jpg and b/public/assets/img/previews/local_portfolio_hartools.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg and b/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg and b/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg and b/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg and b/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg and b/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg and b/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_live-translation.jpg and b/public/assets/img/previews/local_portfolio_live-translation.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_live-translation.jpg and b/public/assets/img/previews/local_portfolio_live-translation.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg and b/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg and b/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_mp-select-mini.jpg and b/public/assets/img/previews/local_portfolio_mp-select-mini.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_mp-select-mini.jpg and b/public/assets/img/previews/local_portfolio_mp-select-mini.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-community.jpg and b/public/assets/img/previews/local_portfolio_openvibe-community.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-community.jpg and b/public/assets/img/previews/local_portfolio_openvibe-community.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg and b/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg and b/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-live.jpg and b/public/assets/img/previews/local_portfolio_openvibe-live.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-live.jpg and b/public/assets/img/previews/local_portfolio_openvibe-live.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-media.jpg and b/public/assets/img/previews/local_portfolio_openvibe-media.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-media.jpg and b/public/assets/img/previews/local_portfolio_openvibe-media.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-sites.jpg and b/public/assets/img/previews/local_portfolio_openvibe-sites.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-sites.jpg and b/public/assets/img/previews/local_portfolio_openvibe-sites.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_perf-harness.jpg and b/public/assets/img/previews/local_portfolio_perf-harness.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_perf-harness.jpg and b/public/assets/img/previews/local_portfolio_perf-harness.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg and b/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg and b/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_powerchat-payments.jpg and b/public/assets/img/previews/local_portfolio_powerchat-payments.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_powerchat-payments.jpg and b/public/assets/img/previews/local_portfolio_powerchat-payments.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_pytools-workshop.jpg and b/public/assets/img/previews/local_portfolio_pytools-workshop.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_pytools-workshop.jpg and b/public/assets/img/previews/local_portfolio_pytools-workshop.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg and b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg and b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg and b/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg and b/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openre.stream.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openre.stream.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.blog.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.blog.jpg differ
Binary files a/public/assets/img/previews/openvibe.chat.jpg and b/public/assets/img/previews/openvibe.chat.jpg differ
Binary files a/public/assets/img/previews/openvibe.chat.jpg and b/public/assets/img/previews/openvibe.chat.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.codes.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.codes.jpg differ
Binary files a/public/assets/img/previews/openvibe.community.jpg and b/public/assets/img/previews/openvibe.community.jpg differ
Binary files a/public/assets/img/previews/openvibe.community.jpg and b/public/assets/img/previews/openvibe.community.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.coupons.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.coupons.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.deals.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.deals.jpg differ
Binary files a/public/assets/img/previews/openvibe.games.jpg and b/public/assets/img/previews/openvibe.games.jpg differ
Binary files a/public/assets/img/previews/openvibe.games.jpg and b/public/assets/img/previews/openvibe.games.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.host.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.host.jpg differ
Binary files a/public/assets/img/previews/openvibe.live.jpg and b/public/assets/img/previews/openvibe.live.jpg differ
Binary files a/public/assets/img/previews/openvibe.live.jpg and b/public/assets/img/previews/openvibe.live.jpg differ
Binary files a/public/assets/img/previews/openvibe.media.jpg and b/public/assets/img/previews/openvibe.media.jpg differ
Binary files a/public/assets/img/previews/openvibe.media.jpg and b/public/assets/img/previews/openvibe.media.jpg differ
Binary files a/public/assets/img/previews/openvibe.network.jpg and b/public/assets/img/previews/openvibe.network.jpg differ
Binary files a/public/assets/img/previews/openvibe.network.jpg and b/public/assets/img/previews/openvibe.network.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.news.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.news.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.reviews.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.reviews.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.tips.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.tips.jpg differ
Binary files a/public/assets/img/previews/openvibe.tools.jpg and b/public/assets/img/previews/openvibe.tools.jpg differ
Binary files a/public/assets/img/previews/openvibe.tools.jpg and b/public/assets/img/previews/openvibe.tools.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.trade.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.trade.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.vip.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.vip.jpg differ
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/openvibe.wiki.jpg differ
Binary files /dev/null and b/public/assets/img/previews/openvibe.wiki.jpg differ
@@ -13,6 +13,7 @@ const W = 1280, H = 800, SCALE = 0.5; // 640x400 output
@@ -13,6 +13,7 @@ const W = 1280, H = 800, SCALE = 0.5; // 640x400 output
const external = [
const external = [
...profile.network.map((n) => n.url),
...profile.network.map((n) => n.url),
+ ...(profile.networkPlanned || []).map((n) => n.url),
'https://powerchat.live',
'https://powerchat.live',
'https://github.com/OpenVibers',
'https://github.com/OpenVibers',
'https://alexfrison.net',
'https://alexfrison.net',
@@ -5,7 +5,6 @@ import { linkify } from "../lib/linkify";
@@ -5,7 +5,6 @@ import { linkify } from "../lib/linkify";
const LINK_TERMS = {
const LINK_TERMS = {
"OpenVibe": { href: "https://openvibe.network", external: true },
"OpenVibe": { href: "https://openvibe.network", external: true },
"PowerChat": { href: "https://powerchat.live", external: true },
"PowerChat": { href: "https://powerchat.live", external: true },
- "GameServerStats": { href: "/portfolio/gameserverstats" },
};
};
const pitchParts = linkify(profile.pitch, LINK_TERMS);
const pitchParts = linkify(profile.pitch, LINK_TERMS);
const statusNowParts = linkify(profile.status.now, LINK_TERMS);
const statusNowParts = linkify(profile.status.now, LINK_TERMS);
@@ -43,7 +43,7 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
@@ -43,7 +43,7 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
<div class="pcard__top">
<div class="pcard__top">
<span class="pcard__icon"><i class={p.data.icon ?? "fa-solid fa-cube"}></i></span>
<span class="pcard__icon"><i class={p.data.icon ?? "fa-solid fa-cube"}></i></span>
<span class="pcard__cat">{catLabel[p.data.category] ?? p.data.category}</span>
<span class="pcard__cat">{catLabel[p.data.category] ?? p.data.category}</span>
- {p.data.year && <span class="pcard__year mono">{p.data.year}</span>}
+ {p.data.status === "coming-soon" ? <span class="pcard__soon mono"><i class="fa-solid fa-clock"></i> Coming soon</span> : p.data.year && <span class="pcard__year mono">{p.data.year}</span>}
</div>
</div>
<h3 class="pcard__title">{p.data.title}</h3>
<h3 class="pcard__title">{p.data.title}</h3>
<p class="pcard__sum">{p.data.summary}</p>
<p class="pcard__sum">{p.data.summary}</p>
@@ -108,6 +108,7 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
@@ -108,6 +108,7 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
.cat-infra .pcard__icon { background: rgba(139, 92, 246, .14); color: var(--violet); border-color: rgba(139, 92, 246, .25); }
.cat-infra .pcard__icon { background: rgba(139, 92, 246, .14); color: var(--violet); border-color: rgba(139, 92, 246, .25); }
.pcard__cat { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.pcard__cat { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.pcard__year { margin-left: auto; font-size: .7rem; color: var(--dim); }
.pcard__year { margin-left: auto; font-size: .7rem; color: var(--dim); }
+ .pcard__soon { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; padding: 3px 9px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }
.pcard__title { margin: 0; font-size: 1.15rem; }
.pcard__title { margin: 0; font-size: 1.15rem; }
.pcard__sum { margin: 0; font-size: .9rem; color: var(--muted); line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.pcard__sum { margin: 0; font-size: .9rem; color: var(--muted); line-height: 1.55; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.pcard__tech { list-style: none; margin: auto 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.pcard__tech { list-style: none; margin: auto 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
@@ -28,25 +28,25 @@ const groups = [
@@ -28,25 +28,25 @@ const groups = [
key: "openvibe", tone: "a", anchor: "network", icon: "fa-solid fa-diagram-project",
key: "openvibe", tone: "a", anchor: "network", icon: "fa-solid fa-diagram-project",
title: "The OpenVibe network", blurb: "Seven live services behind one sign-in, all built and run by me since 2026.",
title: "The OpenVibe network", blurb: "Seven live services behind one sign-in, all built and run by me since 2026.",
count: profile.network.length, allHref: "/portfolio/?cat=openvibe,infra", allLabel: "All OpenVibe case notes",
count: profile.network.length, allHref: "/portfolio/?cat=openvibe,infra", allLabel: "All OpenVibe case notes",
- items: [] as { href: string; external: boolean; image?: string | null; icon: string; title: string; sub: string; text: string; meta: string }[],
+ items: [] as { href: string; external: boolean; image?: string | null; icon: string; title: string; sub: string; text: string; meta: string; soon?: boolean }[],
},
},
{
{
key: "powerchat", tone: "b", icon: "fa-solid fa-bolt",
key: "powerchat", tone: "b", icon: "fa-solid fa-bolt",
title: "PowerChat", blurb: "Contract platform work for a creator-monetization product: payments and the developer API.",
title: "PowerChat", blurb: "Contract platform work for a creator-monetization product: payments and the developer API.",
count: countCats(["powerchat"]), allHref: "/portfolio/?cat=powerchat", allLabel: "All PowerChat work",
count: countCats(["powerchat"]), allHref: "/portfolio/?cat=powerchat", allLabel: "All PowerChat work",
- items: byCats(["powerchat"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "" })),
+ items: byCats(["powerchat"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "", soon: p.data.status === "coming-soon" })),
},
},
{
{
key: "ai", tone: "c", icon: "fa-solid fa-robot",
key: "ai", tone: "c", icon: "fa-solid fa-robot",
title: "AI, data & automation", blurb: "Agents, analytics and tooling that did real work at Amazon and inside OpenVibe.",
title: "AI, data & automation", blurb: "Agents, analytics and tooling that did real work at Amazon and inside OpenVibe.",
count: countCats(["ai", "amazon"]), allHref: "/portfolio/?cat=ai,amazon", allLabel: "All AI & data work",
count: countCats(["ai", "amazon"]), allHref: "/portfolio/?cat=ai,amazon", allLabel: "All AI & data work",
- items: byCats(["ai", "amazon"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "" })),
+ items: byCats(["ai", "amazon"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "", soon: p.data.status === "coming-soon" })),
},
},
{
{
key: "more", tone: "d", icon: "fa-solid fa-shapes",
key: "more", tone: "d", icon: "fa-solid fa-shapes",
title: "Games, tools & side projects", blurb: "The builds that keep the tools sharp: a multiplayer game, desktop apps, hardware and utilities.",
title: "Games, tools & side projects", blurb: "The builds that keep the tools sharp: a multiplayer game, desktop apps, hardware and utilities.",
count: countCats(["game", "tools", "python", "hardware", "web"]), allHref: "/portfolio/", allLabel: "The whole archive",
count: countCats(["game", "tools", "python", "hardware", "web"]), allHref: "/portfolio/", allLabel: "The whole archive",
- items: byCats(["game", "tools", "python", "hardware", "web"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "" })),
+ items: byCats(["game", "tools", "python", "hardware", "web"], 4).map((p) => ({ href: `/portfolio/${p.slug}`, external: false, icon: p.data.icon ?? "fa-solid fa-cube", title: p.data.title, sub: catLabel[p.data.category], text: p.data.summary, meta: p.data.year ?? "", soon: p.data.status === "coming-soon" })),
},
},
];
];
---
---
@@ -63,7 +63,7 @@ const groups = [
@@ -63,7 +63,7 @@ const groups = [
<div class="wk">
<div class="wk">
{groups.map((g, gi) => (
{groups.map((g, gi) => (
- <section class={`wk-group wk-group--${g.tone}`} id={g.anchor} aria-labelledby={`wk-${g.key}`} data-reveal style={`--i:${gi}`}>
+ <section class={`wk-group wk-group--${g.tone}`} id={g.anchor} aria-labelledby={`wk-${g.key}`} style={`--i:${gi}`}>
<header class="wk-group__head">
<header class="wk-group__head">
<span class="wk-group__ico"><i class={g.icon} aria-hidden="true"></i></span>
<span class="wk-group__ico"><i class={g.icon} aria-hidden="true"></i></span>
<div class="wk-group__txt">
<div class="wk-group__txt">
@@ -73,21 +73,31 @@ const groups = [
@@ -73,21 +73,31 @@ const groups = [
<a class="wk-group__all" href={g.allHref}><i class="fa-solid fa-grip" aria-hidden="true"></i> <span>{g.allLabel}</span> <i class="fa-solid fa-arrow-right wk-group__arrow" aria-hidden="true"></i></a>
<a class="wk-group__all" href={g.allHref}><i class="fa-solid fa-grip" aria-hidden="true"></i> <span>{g.allLabel}</span> <i class="fa-solid fa-arrow-right wk-group__arrow" aria-hidden="true"></i></a>
</header>
</header>
{g.key === "openvibe" && <OpenVibeBento />}
{g.key === "openvibe" && <OpenVibeBento />}
+ {g.key === "openvibe" && (
+ <div class="wk-planned" aria-label="Planned OpenVibe sites">
+ <span class="wk-planned__label"><i class="fa-solid fa-clock" aria-hidden="true"></i> Coming soon</span>
+ <ul class="wk-planned__list">
+ {profile.networkPlanned.map((n) => (
+ <li><a href={n.url} target="_blank" rel="noopener" title={`${n.name}: ${n.role} (placeholder page)`}>{n.name.replace("OpenVibe.", ".").replace("OpenRe.", "OpenRe.")}<small>{n.role}</small></a></li>
+ ))}
+ </ul>
+ </div>
+ )}
<ul class="wk-grid" data-stagger hidden={g.items.length === 0}>
<ul class="wk-grid" data-stagger hidden={g.items.length === 0}>
{g.items.map((it) => (
{g.items.map((it) => (
- <li class={`wk-card ${it.image ? "wk-card--img" : ""}`}>
+ <li class={`wk-card ${it.image ? "wk-card--img" : ""} ${it.soon ? "wk-card--soon" : ""}`}>
<a href={it.href} class="wk-card__link" target={it.external ? "_blank" : undefined} rel={it.external ? "noopener" : undefined}>
<a href={it.href} class="wk-card__link" target={it.external ? "_blank" : undefined} rel={it.external ? "noopener" : undefined}>
{it.image && <span class="wk-card__thumb" aria-hidden="true"><img src={it.image} alt="" loading="lazy" width="640" height="240" /></span>}
{it.image && <span class="wk-card__thumb" aria-hidden="true"><img src={it.image} alt="" loading="lazy" width="640" height="240" /></span>}
<span class="wk-card__body">
<span class="wk-card__body">
<span class="wk-card__top">
<span class="wk-card__top">
<span class="wk-card__ico"><i class={it.icon} aria-hidden="true"></i></span>
<span class="wk-card__ico"><i class={it.icon} aria-hidden="true"></i></span>
{!it.image && <span class="wk-card__sub">{it.sub}</span>}
{!it.image && <span class="wk-card__sub">{it.sub}</span>}
- {it.meta && <span class="wk-card__meta mono">{it.meta}</span>}
+ {it.soon ? <span class="wk-card__soon mono"><i class="fa-solid fa-clock" aria-hidden="true"></i> Coming soon</span> : it.meta && <span class="wk-card__meta mono">{it.meta}</span>}
</span>
</span>
<span class="wk-card__title">{it.title}</span>
<span class="wk-card__title">{it.title}</span>
{it.image && <span class="wk-card__role">{it.sub}</span>}
{it.image && <span class="wk-card__role">{it.sub}</span>}
<span class="wk-card__text">{it.text}</span>
<span class="wk-card__text">{it.text}</span>
- <span class="wk-card__go"><span>{it.external ? "Open site" : "Case notes"}</span> <i class={it.external ? "fa-solid fa-arrow-up-right-from-square" : "fa-solid fa-arrow-right"} aria-hidden="true"></i></span>
+ <span class="wk-card__go"><span>{it.soon ? "Preview the plan" : it.external ? "Open site" : "Case notes"}</span> <i class={it.external ? "fa-solid fa-arrow-up-right-from-square" : "fa-solid fa-arrow-right"} aria-hidden="true"></i></span>
</span>
</span>
</a>
</a>
</li>
</li>
@@ -150,6 +160,16 @@ const groups = [
@@ -150,6 +160,16 @@ const groups = [
.wk-card__go i { transition: transform .3s var(--ease); font-size: .7rem; }
.wk-card__go i { transition: transform .3s var(--ease); font-size: .7rem; }
.wk-card__link:hover .wk-card__go i { transform: translateX(3px); }
.wk-card__link:hover .wk-card__go i { transform: translateX(3px); }
.mono { font-family: var(--font-mono); }
.mono { font-family: var(--font-mono); }
+ .wk-card__soon { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: .6rem; letter-spacing: .08em; text-transform: uppercase; padding: 2px 8px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); white-space: nowrap; }
+ .wk-card--soon .wk-card__link { border-style: dashed; }
+ /* Planned domains: every one already has a truthful "coming soon" page, so the chips link there. */
+ .wk-planned { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin-top: 14px; padding: 12px 14px; border-radius: 14px; border: 1px dashed var(--border-strong); background: var(--surface); }
+ .wk-planned__label { display: inline-flex; align-items: center; gap: 7px; font: 600 .68rem var(--font-mono); letter-spacing: .14em; text-transform: uppercase; color: var(--accent-3); }
+ .wk-planned__list { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; }
+ .wk-planned__list a { display: inline-flex; align-items: baseline; gap: 6px; padding: 5px 10px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface-2); color: var(--fg); font: 600 .74rem var(--font-body); text-decoration: none; transition: border-color .25s var(--ease), transform .25s var(--ease); }
+ .wk-planned__list a small { font: 500 .62rem var(--font-mono); color: var(--dim); letter-spacing: .04em; }
+ .wk-planned__list a:hover { text-decoration: none; border-color: rgba(var(--accent-3-rgb), .55); transform: translateY(-1px); }
+ @media (max-width: 560px) { .wk-planned__list a small { display: none; } }
.work__more { margin-top: clamp(22px, 3vw, 34px); display: flex; justify-content: center; }
.work__more { margin-top: clamp(22px, 3vw, 34px); display: flex; justify-content: center; }
.work__all-btn {
.work__all-btn {
@@ -39,6 +39,7 @@ const projects = defineCollection({
@@ -39,6 +39,7 @@ const projects = defineCollection({
video: z.string().url().optional(),
video: z.string().url().optional(),
}).default({}),
}).default({}),
featured: z.boolean().default(false),
featured: z.boolean().default(false),
+ status: z.enum(['live', 'coming-soon', 'archived']).default('live'),
order: z.number().default(0),
order: z.number().default(0),
}),
}),
});
});
@@ -8,7 +8,7 @@
@@ -8,7 +8,7 @@
"bullets": [
"bullets": [
"Built analytics platforms tracking 200,000+ live game servers: ingestion, search, ranking, dashboards and licensed partner APIs. Ranked first on Google for several major titles.",
"Built analytics platforms tracking 200,000+ live game servers: ingestion, search, ranking, dashboards and licensed partner APIs. Ranked first on Google for several major titles.",
"Self-built and collocated 1U and 2U servers in datacenters; designed for scale and DDoS resilience and ran the whole LLC, from licenses and taxes to partner relations.",
"Self-built and collocated 1U and 2U servers in datacenters; designed for scale and DDoS resilience and ran the whole LLC, from licenses and taxes to partner relations.",
- "Rebuilt the idea in 2026 as GameServerLists: a 14-service, game-agnostic server-discovery platform with multi-region probes, time-weighted ranking, an anti-fraud integrity engine and a tiered public API."
+ "Rebuilding the idea in 2026 as GameServerLists (launching soon): a 14-service, game-agnostic server-discovery platform with multi-region probes, time-weighted ranking, an anti-fraud integrity engine and a tiered public API."
],
],
"stack": [
"stack": [
"Node.js",
"Node.js",
@@ -21,4 +21,4 @@
@@ -21,4 +21,4 @@
"NGINX",
"NGINX",
"Linux"
"Linux"
]
]
-}
\ No newline at end of file
\ No newline at end of file
+}
@@ -1,10 +1,11 @@
@@ -1,10 +1,11 @@
---
---
title: "GameServerLists"
title: "GameServerLists"
category: "web"
category: "web"
-summary: "2026 rebuild of the server-tracking idea as a 14-service, game-agnostic discovery platform: multi-region probes, time-weighted ranking, anti-fraud integrity engine and a tiered public API."
+summary: "Launching soon: a 14-service, game-agnostic server-discovery platform with multi-region probes, time-weighted ranking, an anti-fraud integrity engine and a tiered public API."
tech: ["TypeScript", "pnpm monorepo", "TimescaleDB", "Redis", "A2S / Minecraft SLP / EOS", "WebSockets", "Provider-abstracted AI"]
tech: ["TypeScript", "pnpm monorepo", "TimescaleDB", "Redis", "A2S / Minecraft SLP / EOS", "WebSockets", "Provider-abstracted AI"]
role: "Architect · Sole Engineer"
role: "Architect · Sole Engineer"
-year: "2026"
+year: "Coming soon"
+status: "coming-soon"
icon: "fa-solid fa-list-check"
icon: "fa-solid fa-list-check"
order: 13
order: 13
---
---
deleted file mode 100644
deleted file mode 100644
@@ -1,15 +0,0 @@
@@ -1,15 +0,0 @@
-title: "GameServerStats"
-category: "web"
-summary: "Analytics platform tracking 200,000+ live game servers — search, ranking, dashboards, and partner APIs."
-tech: ["Node.js", "PHP", "MongoDB", "Redis", "NGINX", "Linux"]
-role: "Founder / CEO / Sole Engineer"
-year: "2015–2026"
-icon: "fa-solid fa-server"
-order: 30
-
-A decade of analytics for the multiplayer-game-server world. Custom
-ingestion fetches and computes stats on hundreds of thousands of servers,
-serves the #1 Google result for several major games, and licensed to
-third-party sites under written agreement.
@@ -27,6 +27,54 @@
@@ -27,6 +27,54 @@
"img": "/assets/img/previews/openvibe.chat.jpg",
"img": "/assets/img/previews/openvibe.chat.jpg",
"label": "openvibe.chat"
"label": "openvibe.chat"
},
},
+ "openre.stream": {
+ "img": "/assets/img/previews/openre.stream.jpg",
+ "label": "openre.stream"
+ },
+ "openvibe.tips": {
+ "img": "/assets/img/previews/openvibe.tips.jpg",
+ "label": "openvibe.tips"
+ },
+ "openvibe.vip": {
+ "img": "/assets/img/previews/openvibe.vip.jpg",
+ "label": "openvibe.vip"
+ },
+ "openvibe.codes": {
+ "img": "/assets/img/previews/openvibe.codes.jpg",
+ "label": "openvibe.codes"
+ },
+ "openvibe.host": {
+ "img": "/assets/img/previews/openvibe.host.jpg",
+ "label": "openvibe.host"
+ },
+ "openvibe.wiki": {
+ "img": "/assets/img/previews/openvibe.wiki.jpg",
+ "label": "openvibe.wiki"
+ },
+ "openvibe.blog": {
+ "img": "/assets/img/previews/openvibe.blog.jpg",
+ "label": "openvibe.blog"
+ },
+ "openvibe.news": {
+ "img": "/assets/img/previews/openvibe.news.jpg",
+ "label": "openvibe.news"
+ },
+ "openvibe.reviews": {
+ "img": "/assets/img/previews/openvibe.reviews.jpg",
+ "label": "openvibe.reviews"
+ },
+ "openvibe.deals": {
+ "img": "/assets/img/previews/openvibe.deals.jpg",
+ "label": "openvibe.deals"
+ },
+ "openvibe.coupons": {
+ "img": "/assets/img/previews/openvibe.coupons.jpg",
+ "label": "openvibe.coupons"
+ },
+ "openvibe.trade": {
+ "img": "/assets/img/previews/openvibe.trade.jpg",
+ "label": "openvibe.trade"
+ },
"powerchat.live": {
"powerchat.live": {
"img": "/assets/img/previews/powerchat.live.jpg",
"img": "/assets/img/previews/powerchat.live.jpg",
"label": "powerchat.live"
"label": "powerchat.live"
@@ -63,10 +111,6 @@
@@ -63,10 +111,6 @@
"img": "/assets/img/previews/local_portfolio_gameserverlists.jpg",
"img": "/assets/img/previews/local_portfolio_gameserverlists.jpg",
"label": "alexfrison.net/portfolio/gameserverlists"
"label": "alexfrison.net/portfolio/gameserverlists"
},
},
- "path:/portfolio/gameserverstats": {
- "img": "/assets/img/previews/local_portfolio_gameserverstats.jpg",
- "label": "alexfrison.net/portfolio/gameserverstats"
- },
"path:/portfolio/hartools": {
"path:/portfolio/hartools": {
"img": "/assets/img/previews/local_portfolio_hartools.jpg",
"img": "/assets/img/previews/local_portfolio_hartools.jpg",
"label": "alexfrison.net/portfolio/hartools"
"label": "alexfrison.net/portfolio/hartools"
@@ -70,6 +70,21 @@
@@ -70,6 +70,21 @@
{ "key": "community", "name": "OpenVibe.Community", "url": "https://openvibe.community", "role": "Pastes & posts", "blurb": "Server-rendered paste hub with forks, search, feeds and silent SSO, proxied to Live so identity stays consistent.", "stat": "No DB of its own", "image": "/assets/img/sites/community.webp" },
{ "key": "community", "name": "OpenVibe.Community", "url": "https://openvibe.community", "role": "Pastes & posts", "blurb": "Server-rendered paste hub with forks, search, feeds and silent SSO, proxied to Live so identity stays consistent.", "stat": "No DB of its own", "image": "/assets/img/sites/community.webp" },
{ "key": "sites", "name": "OpenVibe.Sites", "url": "https://openvibe.chat", "role": "13 front doors", "blurb": "One catalog and one template generate the pages and nginx configs for openvibe.chat, .codes, .wiki, .news and more.", "stat": "13 domains · 1 build", "image": null }
{ "key": "sites", "name": "OpenVibe.Sites", "url": "https://openvibe.chat", "role": "13 front doors", "blurb": "One catalog and one template generate the pages and nginx configs for openvibe.chat, .codes, .wiki, .news and more.", "stat": "13 domains · 1 build", "image": null }
],
],
+ "networkPlanned": [
+ { "name": "OpenRe.Stream", "url": "https://openre.stream", "role": "Restreaming" },
+ { "name": "OpenVibe.Chat", "url": "https://openvibe.chat", "role": "Chat & calls" },
+ { "name": "OpenVibe.Tips", "url": "https://openvibe.tips", "role": "Creator tips" },
+ { "name": "OpenVibe.VIP", "url": "https://openvibe.vip", "role": "Memberships" },
+ { "name": "OpenVibe.Codes", "url": "https://openvibe.codes", "role": "Developer portal" },
+ { "name": "OpenVibe.Host", "url": "https://openvibe.host", "role": "Hosting" },
+ { "name": "OpenVibe.Wiki", "url": "https://openvibe.wiki", "role": "Wiki" },
+ { "name": "OpenVibe.Blog", "url": "https://openvibe.blog", "role": "Blogs" },
+ { "name": "OpenVibe.News", "url": "https://openvibe.news", "role": "News" },
+ { "name": "OpenVibe.Reviews", "url": "https://openvibe.reviews", "role": "Reviews" },
+ { "name": "OpenVibe.Deals", "url": "https://openvibe.deals", "role": "Deals" },
+ { "name": "OpenVibe.Coupons", "url": "https://openvibe.coupons", "role": "Coupons" },
+ { "name": "OpenVibe.Trade", "url": "https://openvibe.trade", "role": "Markets" }
+ ],
"education": "Snohomish High School, WA. AP Statistics, Calculus, English, Creative Writing and Spanish. Self-taught engineer since age 12: systems, infrastructure, graphics, hardware and whatever the problem needs.",
"education": "Snohomish High School, WA. AP Statistics, Calculus, English, Creative Writing and Spanish. Self-taught engineer since age 12: systems, infrastructure, graphics, hardware and whatever the problem needs.",
"capabilities": ["websites", "platforms", "APIs", "dashboards", "AI features", "automations", "identity systems", "payment systems", "real-time chat", "streaming infra", "whatever you need"]
"capabilities": ["websites", "platforms", "APIs", "dashboards", "AI features", "automations", "identity systems", "payment systems", "real-time chat", "streaming infra", "whatever you need"]
}
}
@@ -248,12 +248,14 @@ const imageUrl = new URL(image, site).toString();
@@ -248,12 +248,14 @@ const imageUrl = new URL(image, site).toString();
const el = en.target as HTMLElement;
const el = en.target as HTMLElement;
const kind = el.dataset.reveal || 'up';
const kind = el.dataset.reveal || 'up';
const delay = Number(el.dataset.delay || 0);
const delay = Number(el.dataset.delay || 0);
- const from: Keyframe = kind === 'left' ? { opacity: 0, transform: 'translateX(-30px)' }
- : kind === 'right' ? { opacity: 0, transform: 'translateX(30px)' }
- : kind === 'scale' ? { opacity: 0, transform: 'scale(.94)' }
- : { opacity: 0, transform: 'translateY(26px)' };
+ // Offsets stay small (and never accumulate on nested [data-reveal] ancestors): larger ones
+ // made cards visibly overlap the next card mid-animation when someone scrolls fast.
+ const from: Keyframe = kind === 'left' ? { opacity: 0, transform: 'translateX(-12px)' }
+ : kind === 'right' ? { opacity: 0, transform: 'translateX(12px)' }
+ : kind === 'scale' ? { opacity: 0, transform: 'scale(.975)' }
+ : { opacity: 0, transform: 'translateY(8px) scale(.99)' };
el.style.opacity = '1'; el.style.transform = 'none';
el.style.opacity = '1'; el.style.transform = 'none';
- el.animate([from, { opacity: 1, transform: 'none' }], { duration: 750, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });
+ el.animate([from, { opacity: 1, transform: 'none' }], { duration: 600, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });
io.unobserve(el);
io.unobserve(el);
}
}
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.06 });
}, { rootMargin: '0px 0px -8% 0px', threshold: 0.06 });
@@ -266,8 +268,8 @@ const imageUrl = new URL(image, site).toString();
@@ -266,8 +268,8 @@ const imageUrl = new URL(image, site).toString();
const kids = Array.from((en.target as HTMLElement).children) as HTMLElement[];
const kids = Array.from((en.target as HTMLElement).children) as HTMLElement[];
kids.forEach((k, i) => {
kids.forEach((k, i) => {
k.style.opacity = '1';
k.style.opacity = '1';
- k.animate([{ opacity: 0, transform: 'translateY(22px)' }, { opacity: 1, transform: 'none' }],
- { duration: 650, delay: i * 70, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });
+ k.animate([{ opacity: 0, transform: 'translateY(8px) scale(.99)' }, { opacity: 1, transform: 'none' }],
+ { duration: 550, delay: Math.min(i, 8) * 55, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });
});
});
sio.unobserve(en.target);
sio.unobserve(en.target);
}
}
@@ -26,7 +26,7 @@ const catLabel: Record<string, string> = {
@@ -26,7 +26,7 @@ const catLabel: Record<string, string> = {
<header class="pp__head" data-reveal>
<header class="pp__head" data-reveal>
<div class="pp__meta-top">
<div class="pp__meta-top">
<span class="chip">{catLabel[d.category] ?? d.category}</span>
<span class="chip">{catLabel[d.category] ?? d.category}</span>
- {d.year && <span class="mono pp__year">{d.year}</span>}
+ {d.status === "coming-soon" ? <span class="mono pp__soon"><i class="fa-solid fa-clock"></i> Coming soon</span> : d.year && <span class="mono pp__year">{d.year}</span>}
</div>
</div>
<h1 class="pp__title">{d.title}</h1>
<h1 class="pp__title">{d.title}</h1>
<p class="pp__lead">{d.summary}</p>
<p class="pp__lead">{d.summary}</p>
@@ -82,6 +82,7 @@ const catLabel: Record<string, string> = {
@@ -82,6 +82,7 @@ const catLabel: Record<string, string> = {
.pp__back:hover { color: var(--fg); text-decoration: none; }
.pp__back:hover { color: var(--fg); text-decoration: none; }
.pp__meta-top { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.pp__meta-top { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.pp__year { color: var(--dim); font-size: .8rem; }
.pp__year { color: var(--dim); font-size: .8rem; }
+ .pp__soon { display: inline-flex; align-items: center; gap: 6px; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }
.pp__title { font-size: clamp(2.2rem, 5.5vw, 4rem); margin: 0 0 .3em; }
.pp__title { font-size: clamp(2.2rem, 5.5vw, 4rem); margin: 0 0 .3em; }
.pp__lead { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--fg); max-width: 62ch; line-height: 1.6; }
.pp__lead { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--fg); max-width: 62ch; line-height: 1.6; }
.pp__meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin: 1.4rem 0; }
.pp__meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin: 1.4rem 0; }
@@ -12,7 +12,6 @@ const projects = (await getCollection("projects")).filter((p) => p.data.featured
@@ -12,7 +12,6 @@ const projects = (await getCollection("projects")).filter((p) => p.data.featured
const LINK_TERMS = {
const LINK_TERMS = {
"OpenVibe": { href: "https://openvibe.network", external: true },
"OpenVibe": { href: "https://openvibe.network", external: true },
"PowerChat": { href: "https://powerchat.live", external: true },
"PowerChat": { href: "https://powerchat.live", external: true },
- "GameServerStats": { href: "/portfolio/gameserverstats" },
};
};
const pitchParts = linkify(profile.pitch, LINK_TERMS);
const pitchParts = linkify(profile.pitch, LINK_TERMS);
---
---