Home: merge the OpenVibe network section into a grouped Work showcase; nav gets Profile; portfolio filter reads ?cat=
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 differnnew file mode 100644BBinary files /dev/null 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 differBBinary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and b/public/assets/img/previews/local_portfolio_gameserverstats.jpg 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-games.jpg and b/public/assets/img/previews/local_portfolio_openvibe-games.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-network.jpg and b/public/assets/img/previews/local_portfolio_openvibe-network.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_openvibe-tools.jpg and b/public/assets/img/previews/local_portfolio_openvibe-tools.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 differBBinary files a/public/assets/img/previews/local_resume.jpg and b/public/assets/img/previews/local_resume.jpg differBBinary files a/public/assets/img/previews/openvibe.chat.jpg and b/public/assets/img/previews/openvibe.chat.jpg differBBinary files a/public/assets/img/previews/openvibe.community.jpg and b/public/assets/img/previews/openvibe.community.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 differ@@@ -38,7 +38,7 @@ const offsets = words.map((_, i) => words.slice(0, i).join(" ").length + (i ? 1</p><div class="hero__cta hero__in" style="--d:800ms">-- <a href="#network" class="btn btn--primary" data-magnetic><i class="fa-solid fa-satellite-dish"></i> See the network <span class="btn__arrow">→</span></a>++ <a href="#work" class="btn btn--primary" data-magnetic><i class="fa-solid fa-layer-group"></i> See what I've built <span class="btn__arrow">→</span></a><a href={profile.resumePdf} class="btn" data-magnetic data-resume-link><i class="fa-solid fa-file-pdf"></i> Resume PDF</a><a href="#contact" class="btn btn--ghost" data-magnetic><i class="fa-solid fa-paper-plane"></i> Get in touch</a></div>@@@ -1,7 +1,7 @@---import profile from "../data/profile.json";const links = [-- { href: "/#network", label: "OpenVibe", id: "network", icon: "fa-solid fa-diagram-project" },++ { href: "/#about", label: "Profile", id: "about", icon: "fa-solid fa-user" },{ href: "/#work", label: "Work", id: "work", icon: "fa-solid fa-briefcase" },{ href: "/#experience", label: "Experience", id: "experience", icon: "fa-solid fa-clock-rotate-left" },{ href: "/#skills", label: "Skills", id: "skills", icon: "fa-solid fa-code" },ddeleted file mode 100644@@@ -1,172 +0,0 @@--import profile from "../data/profile.json";--const sites = profile.network;--const big = sites[0];--const rest = sites.slice(1);--const icons: Record<string, string> = {-- live: "fa-solid fa-tower-broadcast", network: "fa-solid fa-id-badge", media: "fa-solid fa-photo-film",-- tools: "fa-solid fa-toolbox", games: "fa-solid fa-gamepad", community: "fa-solid fa-comments", sites: "fa-solid fa-globe",--};--// Architecture diagram geometry (viewBox 800 x 420)--const hub = { x: 400, y: 210 };--const nodes = [-- { key: "live", label: "Live", x: 130, y: 90 },-- { key: "media", label: "Media", x: 130, y: 330 },-- { key: "tools", label: "Tools", x: 400, y: 50 },-- { key: "games", label: "Games", x: 400, y: 372 },-- { key: "community", label: "Community", x: 670, y: 90 },-- { key: "sites", label: "13 Sites", x: 670, y: 330 },--];--const edges = [-- ["live", "media"],--];--<section class="section network" id="network" aria-labelledby="network-title">-- <div class="container">-- <div class="sec-head">-- <span class="sec-num" aria-hidden="true">02</span>-- <div>-- <div class="sec-index">The OpenVibe network</div>-- <h2 class="sec-title" id="network-title">Seven services. One sign-in. <em>One engineer.</em></h2>-- </div>-- <p class="sec-lead">An open, community-run streaming and tools network I started in 2026 and grew from one streaming site into seven services. Every service below is designed, built, deployed and operated by me, and every one is live.</p>-- </div>---- <div class="bento">-- <a class="tile tile--big spot" href={big.url} target="_blank" rel="noopener" data-reveal data-tilt style={`--img:url(${big.image})`}>-- <div class="tile__shot" aria-hidden="true"><img src={big.image} alt="" loading="lazy" width="1120" height="700" /></div>-- <div class="tile__body">-- <div class="tile__top">-- <span class="tile__icon"><i class={icons[big.key]}></i></span>-- <span class="tile__role">{big.role}</span>-- </div>-- <h3 class="tile__name">{big.name}</h3>-- <p class="tile__blurb">{big.blurb}</p>-- <div class="tile__foot"><span class="tile__stat mono">{big.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>-- </div>-- </a>---- {rest.map((s, i) => (-- <a class={`tile spot tile--${s.key}`} href={s.url} target="_blank" rel="noopener" data-reveal data-delay={String(60 * (i + 1))} data-tilt>-- {s.image && <div class="tile__shot" aria-hidden="true"><img src={s.image} alt="" loading="lazy" width="1120" height="700" /></div>}-- <div class="tile__body">-- <div class="tile__top">-- <span class="tile__icon"><i class={icons[s.key]}></i></span>-- <span class="tile__role">{s.role}</span>-- </div>-- <h3 class="tile__name">{s.name}</h3>-- <p class="tile__blurb">{s.blurb}</p>-- <div class="tile__foot"><span class="tile__stat mono">{s.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>-- </div>-- </a>-- ))}-- </div>---- <div class="arch card" data-reveal>-- <div class="arch__copy">-- <div class="eyebrow"><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> How it fits together</div>-- <h3>One login, one wallet, seven products that feel like one.</h3>-- <p>Sign in once and every OpenVibe site knows who you are, what you own and what you look like. The identity service in the middle handles accounts, security and the shared virtual currency; every other product plugs into it through clear, documented APIs. That's the same playbook I bring to any company: secure single sign-on, payments and APIs designed so new products can be added without breaking the old ones.</p>-- <ul class="arch__facts">-- <li><i class="fa-solid fa-rotate"></i> Updates ship with zero downtime, so users never see an outage</li>-- <li><i class="fa-solid fa-cloud-arrow-up"></i> Media storage tiers automatically from fast local disks to cheap cloud storage</li>-- <li><i class="fa-solid fa-robot"></i> AI features run on a budget-capped, vendor-neutral layer (OpenAI, Anthropic or local models)</li>-- <li><i class="fa-solid fa-shield-halved"></i> Every migration is rehearsed as a dry run before it touches real users</li>-- </ul>-- </div>-- <svg class="arch__svg" viewBox="0 0 800 420" role="img" aria-label="Diagram: OpenVibe.Network at the center connected to Live, Media, Tools, Games, Community and 13 static sites; Live also talks to Media.">-- <defs>-- <linearGradient id="agrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="var(--accent)"/><stop offset="1" stop-color="var(--accent-2)"/></linearGradient>-- <filter id="aglow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="6" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>-- </defs>-- {nodes.map((n) => (-- <g class="arch__edge">-- <line x1={hub.x} y1={hub.y} x2={n.x} y2={n.y} class="arch__line" />-- <line x1={hub.x} y1={hub.y} x2={n.x} y2={n.y} class="arch__pulse" />-- </g>-- ))}-- {edges.map(([a, b]) => { const A = nodes.find(n => n.key === a)!, B = nodes.find(n => n.key === b)!; return (-- <g class="arch__edge arch__edge--side"><line x1={A.x} y1={A.y} x2={B.x} y2={B.y} class="arch__line" /><line x1={A.x} y1={A.y} x2={B.x} y2={B.y} class="arch__pulse arch__pulse--slow" /></g>-- ); })}-- <g class="arch__hub" transform={`translate(${hub.x} ${hub.y})`}>-- <circle r="58" class="arch__hub-glow" filter="url(#aglow)" />-- <circle r="46" class="arch__hub-ring" />-- <circle r="40" class="arch__hub-fill" />-- <text y="-4" text-anchor="middle" class="arch__hub-t1">OpenVibe</text>-- <text y="14" text-anchor="middle" class="arch__hub-t2">.Network</text>-- <text y="30" text-anchor="middle" class="arch__hub-t3">OIDC · wallet · chrome</text>-- </g>-- {nodes.map((n) => (-- <g class="arch__node" transform={`translate(${n.x} ${n.y})`}>-- <rect x="-62" y="-22" width="124" height="44" rx="12" class="arch__node-box" />-- <text y="5" text-anchor="middle" class="arch__node-t">{n.label}</text>-- </g>-- ))}-- </svg>-- </div>-- </div>--</section>----<style>-- .bento { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: minmax(210px, auto); gap: 14px; margin-bottom: 18px; }-- .tile {-- position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end;-- min-height: 210px; border-radius: var(--radius-lg); text-decoration: none; color: var(--fg);-- background: var(--surface); border: 1px solid var(--border);-- transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease);-- will-change: transform;-- }-- .tile:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .45); box-shadow: var(--shadow-card), var(--shadow-glow); }-- .tile--big { grid-column: span 2; grid-row: span 2; min-height: 440px; }-- .tile__shot { position: absolute; inset: 0; z-index: 0; overflow: hidden; }-- .tile__shot img { width: 100%; height: 100%; object-fit: cover; object-position: top; opacity: .28; transform: scale(1.04); transition: transform 1.2s var(--ease), opacity .6s var(--ease), filter .6s; filter: saturate(.8); }-- .tile:hover .tile__shot img { opacity: .55; transform: scale(1) translateY(-2%); filter: saturate(1.05); }-- .tile__shot::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(7,9,15,.1) 0%, rgba(7,9,15,.55) 45%, rgba(7,9,15,.96) 100%); }-- :root[data-theme="light"] .tile__shot::after { background: linear-gradient(180deg, rgba(244,246,251,.2) 0%, rgba(244,246,251,.7) 45%, rgba(244,246,251,.98) 100%); }-- .tile__body { position: relative; z-index: 1; padding: clamp(18px, 2.4vw, 26px); display: flex; flex-direction: column; gap: 8px; }-- .tile__top { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }-- .tile__icon { width: 34px; height: 34px; border-radius: 10px; display: inline-grid; place-items: center; background: rgba(var(--accent-rgb), .14); color: var(--accent); font-size: .9rem; border: 1px solid rgba(var(--accent-rgb), .2); }-- .tile__role { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }-- .tile__foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 6px; }-- .tile__stat { font-size: .7rem; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }-- .tile__name { margin: 0; font-size: clamp(1.1rem, 1.6vw, 1.45rem); letter-spacing: -.02em; overflow-wrap: anywhere; }-- .tile--big .tile__name { font-size: clamp(1.8rem, 3vw, 2.6rem); }-- .tile__blurb { margin: 0; font-size: .92rem; color: var(--muted); line-height: 1.55; max-width: 52ch; }-- .tile__go { display: inline-flex; align-items: center; gap: 8px; flex: none; font: 600 .8rem var(--font-mono); color: var(--accent); opacity: 0; transform: translateY(6px); transition: opacity .35s var(--ease), transform .35s var(--ease); }-- .tile:hover .tile__go { opacity: 1; transform: none; }-- .tile--community, .tile--sites { grid-column: span 2; }-- .tile--sites { background: var(--grad-soft); }-- .tile--sites::before { content: ""; position: absolute; inset: 0; background: radial-gradient(60% 60% at 80% 20%, rgba(var(--accent-2-rgb), .25), transparent 70%); }-- @media (max-width: 1024px) { .bento { grid-template-columns: repeat(2, 1fr); } .tile--big { grid-column: span 2; grid-row: span 1; min-height: 360px; } .tile--community, .tile--sites { grid-column: span 1; } }-- @media (max-width: 560px) { .bento { grid-template-columns: 1fr; } .tile--big, .tile--community, .tile--sites { grid-column: span 1; } .tile--big { min-height: 320px; } }---- /* Architecture */-- .arch { display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(20px, 4vw, 48px); align-items: center; padding: clamp(22px, 3.5vw, 40px); margin-top: clamp(16px, 3vw, 28px); }-- .arch .eyebrow { display: inline-flex; align-items: center; gap: 8px; }-- .arch__copy h3 { font-size: clamp(1.3rem, 2.2vw, 1.8rem); margin-bottom: .6em; }-- .arch__facts { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }-- .arch__facts li { display: flex; gap: 10px; align-items: flex-start; color: var(--muted); font-size: .9rem; }-- .arch__facts i { color: var(--accent-3); margin-top: 4px; font-size: .75rem; }-- .arch__svg { width: 100%; height: auto; overflow: visible; }-- .arch__line { stroke: var(--border-strong); stroke-width: 1.2; }-- .arch__pulse { stroke: url(#agrad); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 14 200; animation: pulse 3.2s linear infinite; opacity: .9; }-- .arch__pulse--slow { animation-duration: 4.4s; animation-delay: -1s; }-- .arch__edge:nth-child(2) .arch__pulse { animation-delay: -.6s; }-- .arch__edge:nth-child(3) .arch__pulse { animation-delay: -1.2s; }-- .arch__edge:nth-child(4) .arch__pulse { animation-delay: -1.8s; }-- .arch__edge:nth-child(5) .arch__pulse { animation-delay: -2.4s; }-- .arch__edge:nth-child(6) .arch__pulse { animation-delay: -3s; }-- @keyframes pulse { from { stroke-dashoffset: 214; } to { stroke-dashoffset: -214; } }-- .arch__hub-glow { fill: rgba(var(--accent-rgb), .16); animation: hubGlow 3s ease-in-out infinite; }-- @keyframes hubGlow { 50% { fill: rgba(var(--accent-rgb), .3); } }-- .arch__hub-ring { fill: none; stroke: url(#agrad); stroke-width: 1.5; stroke-dasharray: 6 8; animation: ringSpin 30s linear infinite; transform-origin: center; transform-box: fill-box; }-- @keyframes ringSpin { to { transform: rotate(360deg); } }-- .arch__hub-fill { fill: var(--surface-solid); stroke: var(--border-strong); }-- .arch__hub-t1 { font: 700 15px var(--font-display); fill: var(--fg); }-- .arch__hub-t2 { font: 700 13px var(--font-display); fill: var(--accent); }-- .arch__hub-t3 { font: 500 8px var(--font-mono); fill: var(--dim); letter-spacing: .06em; }-- .arch__node-box { fill: var(--surface-solid); stroke: var(--border-strong); transition: stroke .3s, fill .3s; }-- .arch__node:hover .arch__node-box { stroke: var(--accent); }-- .arch__node-t { font: 600 13px var(--font-body); fill: var(--fg); }-- @media (max-width: 900px) { .arch { grid-template-columns: 1fr; } }--</style>@@@ -60,18 +60,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));))}</ul>-- {limit && (-- <div class="work__more" data-reveal="scale">-- <a href="/portfolio" class="work__all-btn" data-magnetic>-- <span class="work__all-ico"><i class="fa-solid fa-layer-group"></i></span>-- <span class="work__all-text">-- <strong>See all {all.length} projects</strong>-- <small>OpenVibe, PowerChat, Amazon, side builds and more — the full case-note archive</small>-- </span>-- <span class="work__all-go" aria-hidden="true"><i class="fa-solid fa-arrow-right"></i></span>-- </a>-- </div>-- )}</div></section>@@@ -129,41 +117,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));.pcard__more { font: 600 .78rem var(--font-mono); color: var(--accent); }.pcard__ext { position: relative; z-index: 3; width: 32px; height: 32px; border-radius: 50%; display: inline-grid; place-items: center; color: var(--muted); border: 1px solid var(--border); transition: all .25s; }.pcard__ext:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; background: rgba(var(--accent-rgb), .12); }-- /* A big, unmistakable card instead of a link easy to scroll past — extra top margin and a-- fade of the section's own background above it, so there's real space to notice it in. */-- .work__more { position: relative; margin-top: clamp(-8px, -1vw, 8px); display: flex; justify-content: center; }-- .work__all-btn {-- position: relative; z-index: 1; display: flex; align-items: center; gap: 18px;-- width: min(620px, 100%); padding: 22px 26px; border-radius: 22px;-- background: var(--surface); border: 1px solid var(--border-strong);-- text-decoration: none; color: var(--fg);-- box-shadow: var(--shadow-card);-- transition: transform .4s var(--ease), border-color .4s var(--ease), box-shadow .4s var(--ease), background .4s var(--ease);-- }-- .work__all-btn:hover {-- text-decoration: none; transform: translateY(-4px) scale(1.015);-- border-color: rgba(var(--accent-rgb), .55); box-shadow: var(--shadow-card), var(--shadow-glow); background: var(--surface-2);-- }-- .work__all-ico {-- flex: none; width: 52px; height: 52px; border-radius: 16px; display: grid; place-items: center;-- background: var(--grad); color: var(--on-accent); font-size: 1.25rem; box-shadow: var(--shadow-glow);-- animation: workAllPulse 2.6s var(--ease) infinite;-- }-- @keyframes workAllPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }-- .work__all-text { flex: 1; min-width: 0; display: grid; gap: 3px; }-- .work__all-text strong { font-family: var(--font-display); font-size: 1.2rem; letter-spacing: -.01em; }-- .work__all-text small { color: var(--muted); font-size: .86rem; line-height: 1.4; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }-- .work__all-go {-- flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;-- color: var(--accent); border: 1px solid rgba(var(--accent-rgb), .3); background: rgba(var(--accent-rgb), .08);-- transition: transform .4s var(--ease), background .3s;-- }-- .work__all-btn:hover .work__all-go { transform: translateX(5px); background: rgba(var(--accent-rgb), .16); }-- @media (max-width: 640px) {-- .work__all-btn { padding: 18px 18px; gap: 14px; }-- .work__all-text small { white-space: normal; }-- .work__all-ico { width: 44px; height: 44px; font-size: 1.05rem; }-- }@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }@media (max-width: 600px) { .pgrid { grid-template-columns: 1fr; } }</style>@@@ -172,17 +125,22 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));document.querySelectorAll<HTMLElement>('.work').forEach((sec) => {const btns = sec.querySelectorAll<HTMLButtonElement>('.filter');const cards = sec.querySelectorAll<HTMLElement>('.pcard');-- btns.forEach((b) => b.addEventListener('click', () => {-- const f = b.dataset.filter!;-- btns.forEach((x) => { x.classList.toggle('is-active', x === b); x.setAttribute('aria-selected', String(x === b)); });++ // `f` may be a comma list (the home page's group links pass e.g. "ai,amazon"); a chip is++ // active only when it matches exactly.++ const apply = (f: string) => {++ const set = new Set(f.split(',').map((s) => s.trim()).filter(Boolean));++ btns.forEach((x) => { const on = x.dataset.filter === f; x.classList.toggle('is-active', on); x.setAttribute('aria-selected', String(on)); });let shown = 0;cards.forEach((c) => {-- const show = f === 'all' || c.dataset.cat === f;++ const show = set.has('all') || set.has(c.dataset.cat || '');if (show) {c.classList.remove('is-hidden');c.animate([{ opacity: 0, transform: 'translateY(14px) scale(.98)' }, { opacity: 1, transform: 'none' }], { duration: 450, delay: shown++ * 40, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' });} else c.classList.add('is-hidden');});-- }));++ };++ btns.forEach((b) => b.addEventListener('click', () => { apply(b.dataset.filter!); history.replaceState(null, '', b.dataset.filter === 'all' ? location.pathname : `?cat=${b.dataset.filter}`); }));++ const initial = new URLSearchParams(location.search).get('cat');++ if (initial) apply(initial);});</script>@@@ -6,7 +6,7 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;<section class="section skills" id="skills" aria-labelledby="skills-title"><div class="container"><div class="sec-head">-- <span class="sec-num" aria-hidden="true">05</span>++ <span class="sec-num" aria-hidden="true">04</span><div><div class="sec-index">Skills</div><h2 class="sec-title" id="skills-title">The <em>toolbox</em>, honestly labelled.</h2>@@@ -9,7 +9,7 @@ const kindIcon: Record<string, string> = {<section class="section xp" id="experience" aria-labelledby="xp-title"><div class="container"><div class="sec-head">-- <span class="sec-num" aria-hidden="true">04</span>++ <span class="sec-num" aria-hidden="true">03</span><div><div class="sec-index">Experience</div><h2 class="sec-title" id="xp-title">A decade of <em>owning the outcome</em>.</h2>nnew file mode 100644@@@ -0,0 +1,170 @@++---++// The home page's single "Work" section: the OpenVibe network and the project archive in one++// place, split into clearly labelled groups (each with its own color, header and "view all")++// instead of a separate network section followed by an ungrouped grid. Cards are deliberately++// compact — one line of title, two of summary — with the full case notes one click away.++import { getCollection } from "astro:content";++import profile from "../data/profile.json";++++const all = (await getCollection("projects")).sort((a, b) => a.data.order - b.data.order);++const byCats = (cats: string[], n: number) =>++ all.filter((p) => cats.includes(p.data.category)).sort((a, b) => Number(b.data.featured) - Number(a.data.featured) || a.data.order - b.data.order).slice(0, n);++const countCats = (cats: string[]) => all.filter((p) => cats.includes(p.data.category)).length;++++const catLabel: Record<string, string> = {++ openvibe: "OpenVibe", powerchat: "PowerChat", amazon: "Amazon", ai: "AI", infra: "Infra & ops",++ game: "Games", tools: "Tools", python: "Python", hardware: "Hardware", web: "Web",++};++const netIcon: Record<string, string> = {++ live: "fa-solid fa-tower-broadcast", network: "fa-solid fa-id-badge", media: "fa-solid fa-photo-film", tools: "fa-solid fa-toolbox",++ games: "fa-solid fa-gamepad", community: "fa-solid fa-comments", sites: "fa-solid fa-globe",++};++++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: profile.network.map((n) => ({ href: n.url, external: true, image: n.image, icon: netIcon[n.key] ?? "fa-solid fa-cube", title: n.name, sub: n.role, text: n.blurb, meta: n.stat })),++ },++ {++ 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 ?? "" })),++ },++ {++ 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 ?? "" })),++ },++ {++ 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 ?? "" })),++ },++];++---++<section class="section work" id="work" aria-labelledby="work-title">++ <div class="container">++ <div class="sec-head">++ <span class="sec-num" aria-hidden="true">02</span>++ <div>++ <div class="sec-index">Work</div>++ <h2 class="sec-title" id="work-title">Things I <em>shipped</em>, grouped.</h2>++ </div>++ <p class="sec-lead">Four groups, short cards, real links. Every card opens the case notes; every OpenVibe tile opens the live site.</p>++ </div>++++ <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}`}>++ <header class="wk-group__head">++ <span class="wk-group__ico"><i class={g.icon} aria-hidden="true"></i></span>++ <div class="wk-group__txt">++ <h3 class="wk-group__title" id={`wk-${g.key}`}>{g.title} <span class="wk-group__n mono">{g.count}</span></h3>++ <p class="wk-group__blurb">{g.blurb}</p>++ </div>++ <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>++ <ul class="wk-grid" data-stagger>++ {g.items.map((it) => (++ <li class={`wk-card ${it.image ? "wk-card--img" : ""}`}>++ <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>}++ </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>++ </a>++ </li>++ ))}++ </ul>++ </section>++ ))}++ </div>++++ <div class="work__more" data-reveal="scale">++ <a href="/portfolio/" class="work__all-btn" data-magnetic>++ <span class="work__all-ico"><i class="fa-solid fa-layer-group"></i></span>++ <span class="work__all-text">++ <strong>See all {all.length} projects and every website</strong>++ <small>Filter by area, open the case notes, or jump straight to the live sites</small>++ </span>++ <span class="work__all-go" aria-hidden="true"><i class="fa-solid fa-arrow-right"></i></span>++ </a>++ </div>++ </div>++</section>++++<style>++ .wk { display: grid; gap: clamp(18px, 3vw, 30px); }++ /* Each group carries its own tone so the eye can tell them apart mid-scroll: a colored icon++ square, a matching left rule and a faint tinted background. */++ .wk-group { --g: var(--accent); --g-rgb: var(--accent-rgb); position: relative; padding: clamp(16px, 2.4vw, 24px); border-radius: var(--radius-lg); background: linear-gradient(135deg, rgba(var(--g-rgb), .07), transparent 55%), var(--surface); border: 1px solid var(--border); scroll-margin-top: 90px; }++ .wk-group::before { content: ""; position: absolute; left: -1px; top: 18%; bottom: 18%; width: 3px; border-radius: 3px; background: var(--g); box-shadow: 0 0 18px rgba(var(--g-rgb), .7); }++ .wk-group--b { --g: var(--accent-2); --g-rgb: var(--accent-2-rgb); }++ .wk-group--c { --g: var(--accent-3); --g-rgb: var(--accent-3-rgb); }++ .wk-group--d { --g: var(--violet); --g-rgb: 139, 92, 246; }++ .wk-group__head { display: grid; grid-template-columns: auto 1fr auto; gap: 14px 16px; align-items: center; margin-bottom: 16px; }++ .wk-group__ico { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); font-size: 1.05rem; box-shadow: 0 10px 24px -14px rgba(var(--g-rgb), .9); }++ .wk-group__txt { min-width: 0; }++ .wk-group__title { margin: 0; font-size: clamp(1.15rem, 2vw, 1.45rem); display: flex; align-items: center; gap: 10px; }++ .wk-group__n { font-size: .72rem; padding: 2px 8px; border-radius: 999px; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); }++ .wk-group__blurb { margin: 3px 0 0; color: var(--muted); font-size: .92rem; line-height: 1.5; }++ .wk-group__all { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border-radius: 999px; border: 1px solid rgba(var(--g-rgb), .35); background: rgba(var(--g-rgb), .08); color: var(--fg); font: 600 .8rem var(--font-body); text-decoration: none; white-space: nowrap; transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease); }++ .wk-group__all i:first-child { color: var(--g); }++ .wk-group__arrow { transition: transform .3s var(--ease); font-size: .75rem; }++ .wk-group__all:hover { text-decoration: none; background: rgba(var(--g-rgb), .16); border-color: rgba(var(--g-rgb), .6); transform: translateY(-1px); }++ .wk-group__all:hover .wk-group__arrow { transform: translateX(3px); }++++ .wk-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }++ .wk-card { min-width: 0; }++ .wk-card__link { display: flex; flex-direction: column; height: 100%; border-radius: 16px; overflow: hidden; background: var(--surface); border: 1px solid var(--border); color: var(--fg); text-decoration: none; transition: transform .4s var(--ease), border-color .3s var(--ease), box-shadow .4s var(--ease), background .3s var(--ease); }++ .wk-card__link:hover { text-decoration: none; transform: translateY(-3px); border-color: rgba(var(--g-rgb), .5); background: var(--surface-2); box-shadow: var(--shadow-card), 0 0 0 1px rgba(var(--g-rgb), .12), 0 16px 40px -22px rgba(var(--g-rgb), .7); }++ .wk-card__thumb { display: block; aspect-ratio: 16 / 6; overflow: hidden; background: var(--bg-2); border-bottom: 1px solid var(--border); }++ .wk-card__thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; transition: transform 1s var(--ease); }++ .wk-card__link:hover .wk-card__thumb img { transform: scale(1.06); }++ .wk-card__body { display: flex; flex-direction: column; gap: 6px; padding: 12px 13px 12px; flex: 1; }++ .wk-card__top { display: flex; align-items: center; gap: 8px; }++ .wk-card__ico { width: 28px; height: 28px; border-radius: 9px; display: inline-grid; place-items: center; background: rgba(var(--g-rgb), .13); color: var(--g); font-size: .78rem; flex: none; }++ .wk-card__sub { font: 500 .66rem var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++ .wk-card__meta { margin-left: auto; font-size: .66rem; color: var(--dim); white-space: nowrap; }++ .wk-card__title { font: 700 .98rem/1.25 var(--font-display); letter-spacing: -.01em; }++ .wk-card__role { font: 500 .66rem var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--g); margin-top: -2px; }++ .wk-card__text { color: var(--muted); font-size: .82rem; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }++ .wk-card__go { margin-top: auto; padding-top: 4px; display: inline-flex; align-items: center; gap: 6px; font: 600 .72rem var(--font-mono); color: var(--g); }++ .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); }++++ .work__more { margin-top: clamp(22px, 3vw, 34px); display: flex; justify-content: center; }++ .work__all-btn {++ position: relative; z-index: 1; display: flex; align-items: center; gap: 18px;++ width: min(640px, 100%); padding: 22px 26px; border-radius: 22px;++ background: var(--surface); border: 1px solid var(--border-strong);++ text-decoration: none; color: var(--fg); box-shadow: var(--shadow-card);++ transition: transform .4s var(--ease), border-color .4s var(--ease), box-shadow .4s var(--ease), background .4s var(--ease);++ }++ .work__all-btn:hover { text-decoration: none; transform: translateY(-4px) scale(1.015); border-color: rgba(var(--accent-rgb), .55); box-shadow: var(--shadow-card), var(--shadow-glow); background: var(--surface-2); }++ .work__all-ico { flex: none; width: 52px; height: 52px; border-radius: 16px; display: grid; place-items: center; background: var(--grad); color: var(--on-accent); font-size: 1.25rem; box-shadow: var(--shadow-glow); animation: workAllPulse 2.6s var(--ease) infinite; }++ @keyframes workAllPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }++ .work__all-text { flex: 1; min-width: 0; display: grid; gap: 3px; }++ .work__all-text strong { font-family: var(--font-display); font-size: 1.2rem; letter-spacing: -.01em; }++ .work__all-text small { color: var(--muted); font-size: .86rem; line-height: 1.4; }++ .work__all-go { flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; color: var(--accent); border: 1px solid rgba(var(--accent-rgb), .3); background: rgba(var(--accent-rgb), .08); transition: transform .4s var(--ease), background .3s; }++ .work__all-btn:hover .work__all-go { transform: translateX(5px); background: rgba(var(--accent-rgb), .16); }++++ @media (max-width: 1100px) { .wk-grid { grid-template-columns: repeat(3, 1fr); } }++ @media (max-width: 820px) { .wk-grid { grid-template-columns: repeat(2, 1fr); } .wk-group__head { grid-template-columns: auto 1fr; } .wk-group__all { grid-column: 1 / -1; justify-self: start; } }++ @media (max-width: 480px) { .wk-grid { grid-template-columns: 1fr 1fr; gap: 8px; } .wk-card__body { padding: 10px 11px; } .wk-card__text { display: none; } .wk-card__title { font-size: .9rem; } .wk-card--img .wk-card__meta { display: none; } .work__all-btn { padding: 18px; gap: 14px; } .work__all-ico { width: 44px; height: 44px; font-size: 1.05rem; } }++</style>@@@ -151,9 +151,9 @@"img": "/assets/img/previews/local_portfolio_youtube-desktop-client.jpg","label": "alexfrison.net/portfolio/youtube-desktop-client"},-- "path:/#network": {-- "img": "/assets/img/previews/local_home__network.jpg",-- "label": "alexfrison.net#network"++ "path:/#about": {++ "img": "/assets/img/previews/local_home__about.jpg",++ "label": "alexfrison.net#about"},"path:/#work": {"img": "/assets/img/previews/local_home__work.jpg",@@@ -171,6 +171,10 @@"img": "/assets/img/previews/local_home__contact.jpg","label": "alexfrison.net#contact"},++ "path:/#network": {++ "img": "/assets/img/previews/local_home__network.jpg",++ "label": "alexfrison.net#network"++ },"path:/#metrics": {"img": "/assets/img/previews/local_home__metrics.jpg","label": "alexfrison.net#metrics"@@@ -4,8 +4,7 @@ import Hero from "../components/Hero.astro";import Metrics from "../components/Metrics.astro";import Marquee from "../components/Marquee.astro";import Now from "../components/Now.astro";--import Network from "../components/Network.astro";--import ProjectGrid from "../components/ProjectGrid.astro";++import WorkShowcase from "../components/WorkShowcase.astro";import Timeline from "../components/Timeline.astro";import Skills from "../components/Skills.astro";import Contact from "../components/Contact.astro";@@@ -15,8 +14,7 @@ import Contact from "../components/Contact.astro";<Metrics /><Marquee /><Now />-- <Network />-- <ProjectGrid limit={9} />++ <WorkShowcase /><Timeline /><Skills /><Contact />
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
new file mode 100644
new file mode 100644
Binary files /dev/null and b/public/assets/img/previews/local_home__about.jpg differ
Binary files /dev/null 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
Binary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and b/public/assets/img/previews/local_portfolio_gameserverstats.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and b/public/assets/img/previews/local_portfolio_gameserverstats.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_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-games.jpg and b/public/assets/img/previews/local_portfolio_openvibe-games.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-games.jpg and b/public/assets/img/previews/local_portfolio_openvibe-games.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-network.jpg and b/public/assets/img/previews/local_portfolio_openvibe-network.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-network.jpg and b/public/assets/img/previews/local_portfolio_openvibe-network.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_openvibe-tools.jpg and b/public/assets/img/previews/local_portfolio_openvibe-tools.jpg differ
Binary files a/public/assets/img/previews/local_portfolio_openvibe-tools.jpg and b/public/assets/img/previews/local_portfolio_openvibe-tools.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
Binary files a/public/assets/img/previews/local_resume.jpg and b/public/assets/img/previews/local_resume.jpg differ
Binary files a/public/assets/img/previews/local_resume.jpg and b/public/assets/img/previews/local_resume.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
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
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
@@ -38,7 +38,7 @@ const offsets = words.map((_, i) => words.slice(0, i).join(" ").length + (i ? 1
@@ -38,7 +38,7 @@ const offsets = words.map((_, i) => words.slice(0, i).join(" ").length + (i ? 1
</p>
</p>
<div class="hero__cta hero__in" style="--d:800ms">
<div class="hero__cta hero__in" style="--d:800ms">
- <a href="#network" class="btn btn--primary" data-magnetic><i class="fa-solid fa-satellite-dish"></i> See the network <span class="btn__arrow">→</span></a>
+ <a href="#work" class="btn btn--primary" data-magnetic><i class="fa-solid fa-layer-group"></i> See what I've built <span class="btn__arrow">→</span></a>
<a href={profile.resumePdf} class="btn" data-magnetic data-resume-link><i class="fa-solid fa-file-pdf"></i> Resume PDF</a>
<a href={profile.resumePdf} class="btn" data-magnetic data-resume-link><i class="fa-solid fa-file-pdf"></i> Resume PDF</a>
<a href="#contact" class="btn btn--ghost" data-magnetic><i class="fa-solid fa-paper-plane"></i> Get in touch</a>
<a href="#contact" class="btn btn--ghost" data-magnetic><i class="fa-solid fa-paper-plane"></i> Get in touch</a>
</div>
</div>
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
---
---
import profile from "../data/profile.json";
import profile from "../data/profile.json";
const links = [
const links = [
- { href: "/#network", label: "OpenVibe", id: "network", icon: "fa-solid fa-diagram-project" },
+ { href: "/#about", label: "Profile", id: "about", icon: "fa-solid fa-user" },
{ href: "/#work", label: "Work", id: "work", icon: "fa-solid fa-briefcase" },
{ href: "/#work", label: "Work", id: "work", icon: "fa-solid fa-briefcase" },
{ href: "/#experience", label: "Experience", id: "experience", icon: "fa-solid fa-clock-rotate-left" },
{ href: "/#experience", label: "Experience", id: "experience", icon: "fa-solid fa-clock-rotate-left" },
{ href: "/#skills", label: "Skills", id: "skills", icon: "fa-solid fa-code" },
{ href: "/#skills", label: "Skills", id: "skills", icon: "fa-solid fa-code" },
deleted file mode 100644
deleted file mode 100644
@@ -1,172 +0,0 @@
@@ -1,172 +0,0 @@
-import profile from "../data/profile.json";
-const sites = profile.network;
-const big = sites[0];
-const rest = sites.slice(1);
-const icons: Record<string, string> = {
- live: "fa-solid fa-tower-broadcast", network: "fa-solid fa-id-badge", media: "fa-solid fa-photo-film",
- tools: "fa-solid fa-toolbox", games: "fa-solid fa-gamepad", community: "fa-solid fa-comments", sites: "fa-solid fa-globe",
-};
-// Architecture diagram geometry (viewBox 800 x 420)
-const hub = { x: 400, y: 210 };
-const nodes = [
- { key: "live", label: "Live", x: 130, y: 90 },
- { key: "media", label: "Media", x: 130, y: 330 },
- { key: "tools", label: "Tools", x: 400, y: 50 },
- { key: "games", label: "Games", x: 400, y: 372 },
- { key: "community", label: "Community", x: 670, y: 90 },
- { key: "sites", label: "13 Sites", x: 670, y: 330 },
-];
-const edges = [
- ["live", "media"],
-];
-<section class="section network" id="network" aria-labelledby="network-title">
- <div class="container">
- <div class="sec-head">
- <span class="sec-num" aria-hidden="true">02</span>
- <div>
- <div class="sec-index">The OpenVibe network</div>
- <h2 class="sec-title" id="network-title">Seven services. One sign-in. <em>One engineer.</em></h2>
- </div>
- <p class="sec-lead">An open, community-run streaming and tools network I started in 2026 and grew from one streaming site into seven services. Every service below is designed, built, deployed and operated by me, and every one is live.</p>
- </div>
-
- <div class="bento">
- <a class="tile tile--big spot" href={big.url} target="_blank" rel="noopener" data-reveal data-tilt style={`--img:url(${big.image})`}>
- <div class="tile__shot" aria-hidden="true"><img src={big.image} alt="" loading="lazy" width="1120" height="700" /></div>
- <div class="tile__body">
- <div class="tile__top">
- <span class="tile__icon"><i class={icons[big.key]}></i></span>
- <span class="tile__role">{big.role}</span>
- </div>
- <h3 class="tile__name">{big.name}</h3>
- <p class="tile__blurb">{big.blurb}</p>
- <div class="tile__foot"><span class="tile__stat mono">{big.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>
- </div>
- </a>
-
- {rest.map((s, i) => (
- <a class={`tile spot tile--${s.key}`} href={s.url} target="_blank" rel="noopener" data-reveal data-delay={String(60 * (i + 1))} data-tilt>
- {s.image && <div class="tile__shot" aria-hidden="true"><img src={s.image} alt="" loading="lazy" width="1120" height="700" /></div>}
- <div class="tile__body">
- <div class="tile__top">
- <span class="tile__icon"><i class={icons[s.key]}></i></span>
- <span class="tile__role">{s.role}</span>
- </div>
- <h3 class="tile__name">{s.name}</h3>
- <p class="tile__blurb">{s.blurb}</p>
- <div class="tile__foot"><span class="tile__stat mono">{s.stat}</span><span class="tile__go">Open <i class="fa-solid fa-arrow-up-right-from-square"></i></span></div>
- </div>
- </a>
- ))}
- </div>
-
- <div class="arch card" data-reveal>
- <div class="arch__copy">
- <div class="eyebrow"><i class="fa-solid fa-diagram-project" aria-hidden="true"></i> How it fits together</div>
- <h3>One login, one wallet, seven products that feel like one.</h3>
- <p>Sign in once and every OpenVibe site knows who you are, what you own and what you look like. The identity service in the middle handles accounts, security and the shared virtual currency; every other product plugs into it through clear, documented APIs. That's the same playbook I bring to any company: secure single sign-on, payments and APIs designed so new products can be added without breaking the old ones.</p>
- <ul class="arch__facts">
- <li><i class="fa-solid fa-rotate"></i> Updates ship with zero downtime, so users never see an outage</li>
- <li><i class="fa-solid fa-cloud-arrow-up"></i> Media storage tiers automatically from fast local disks to cheap cloud storage</li>
- <li><i class="fa-solid fa-robot"></i> AI features run on a budget-capped, vendor-neutral layer (OpenAI, Anthropic or local models)</li>
- <li><i class="fa-solid fa-shield-halved"></i> Every migration is rehearsed as a dry run before it touches real users</li>
- </ul>
- </div>
- <svg class="arch__svg" viewBox="0 0 800 420" role="img" aria-label="Diagram: OpenVibe.Network at the center connected to Live, Media, Tools, Games, Community and 13 static sites; Live also talks to Media.">
- <defs>
- <linearGradient id="agrad" x1="0" y1="0" x2="1" y2="0"><stop offset="0" stop-color="var(--accent)"/><stop offset="1" stop-color="var(--accent-2)"/></linearGradient>
- <filter id="aglow" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="6" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
- </defs>
- {nodes.map((n) => (
- <g class="arch__edge">
- <line x1={hub.x} y1={hub.y} x2={n.x} y2={n.y} class="arch__line" />
- <line x1={hub.x} y1={hub.y} x2={n.x} y2={n.y} class="arch__pulse" />
- </g>
- ))}
- {edges.map(([a, b]) => { const A = nodes.find(n => n.key === a)!, B = nodes.find(n => n.key === b)!; return (
- <g class="arch__edge arch__edge--side"><line x1={A.x} y1={A.y} x2={B.x} y2={B.y} class="arch__line" /><line x1={A.x} y1={A.y} x2={B.x} y2={B.y} class="arch__pulse arch__pulse--slow" /></g>
- ); })}
- <g class="arch__hub" transform={`translate(${hub.x} ${hub.y})`}>
- <circle r="58" class="arch__hub-glow" filter="url(#aglow)" />
- <circle r="46" class="arch__hub-ring" />
- <circle r="40" class="arch__hub-fill" />
- <text y="-4" text-anchor="middle" class="arch__hub-t1">OpenVibe</text>
- <text y="14" text-anchor="middle" class="arch__hub-t2">.Network</text>
- <text y="30" text-anchor="middle" class="arch__hub-t3">OIDC · wallet · chrome</text>
- </g>
- {nodes.map((n) => (
- <g class="arch__node" transform={`translate(${n.x} ${n.y})`}>
- <rect x="-62" y="-22" width="124" height="44" rx="12" class="arch__node-box" />
- <text y="5" text-anchor="middle" class="arch__node-t">{n.label}</text>
- </g>
- ))}
- </svg>
- </div>
- </div>
-</section>
-
-<style>
- .bento { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: minmax(210px, auto); gap: 14px; margin-bottom: 18px; }
- .tile {
- position: relative; overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end;
- min-height: 210px; border-radius: var(--radius-lg); text-decoration: none; color: var(--fg);
- background: var(--surface); border: 1px solid var(--border);
- transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease);
- will-change: transform;
- }
- .tile:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .45); box-shadow: var(--shadow-card), var(--shadow-glow); }
- .tile--big { grid-column: span 2; grid-row: span 2; min-height: 440px; }
- .tile__shot { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
- .tile__shot img { width: 100%; height: 100%; object-fit: cover; object-position: top; opacity: .28; transform: scale(1.04); transition: transform 1.2s var(--ease), opacity .6s var(--ease), filter .6s; filter: saturate(.8); }
- .tile:hover .tile__shot img { opacity: .55; transform: scale(1) translateY(-2%); filter: saturate(1.05); }
- .tile__shot::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, rgba(7,9,15,.1) 0%, rgba(7,9,15,.55) 45%, rgba(7,9,15,.96) 100%); }
- :root[data-theme="light"] .tile__shot::after { background: linear-gradient(180deg, rgba(244,246,251,.2) 0%, rgba(244,246,251,.7) 45%, rgba(244,246,251,.98) 100%); }
- .tile__body { position: relative; z-index: 1; padding: clamp(18px, 2.4vw, 26px); display: flex; flex-direction: column; gap: 8px; }
- .tile__top { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; }
- .tile__icon { width: 34px; height: 34px; border-radius: 10px; display: inline-grid; place-items: center; background: rgba(var(--accent-rgb), .14); color: var(--accent); font-size: .9rem; border: 1px solid rgba(var(--accent-rgb), .2); }
- .tile__role { font-family: var(--font-mono); font-size: .7rem; letter-spacing: .12em; text-transform: uppercase; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
- .tile__foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 6px; }
- .tile__stat { font-size: .7rem; color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
- .tile__name { margin: 0; font-size: clamp(1.1rem, 1.6vw, 1.45rem); letter-spacing: -.02em; overflow-wrap: anywhere; }
- .tile--big .tile__name { font-size: clamp(1.8rem, 3vw, 2.6rem); }
- .tile__blurb { margin: 0; font-size: .92rem; color: var(--muted); line-height: 1.55; max-width: 52ch; }
- .tile__go { display: inline-flex; align-items: center; gap: 8px; flex: none; font: 600 .8rem var(--font-mono); color: var(--accent); opacity: 0; transform: translateY(6px); transition: opacity .35s var(--ease), transform .35s var(--ease); }
- .tile:hover .tile__go { opacity: 1; transform: none; }
- .tile--community, .tile--sites { grid-column: span 2; }
- .tile--sites { background: var(--grad-soft); }
- .tile--sites::before { content: ""; position: absolute; inset: 0; background: radial-gradient(60% 60% at 80% 20%, rgba(var(--accent-2-rgb), .25), transparent 70%); }
- @media (max-width: 1024px) { .bento { grid-template-columns: repeat(2, 1fr); } .tile--big { grid-column: span 2; grid-row: span 1; min-height: 360px; } .tile--community, .tile--sites { grid-column: span 1; } }
- @media (max-width: 560px) { .bento { grid-template-columns: 1fr; } .tile--big, .tile--community, .tile--sites { grid-column: span 1; } .tile--big { min-height: 320px; } }
-
- /* Architecture */
- .arch { display: grid; grid-template-columns: .9fr 1.1fr; gap: clamp(20px, 4vw, 48px); align-items: center; padding: clamp(22px, 3.5vw, 40px); margin-top: clamp(16px, 3vw, 28px); }
- .arch .eyebrow { display: inline-flex; align-items: center; gap: 8px; }
- .arch__copy h3 { font-size: clamp(1.3rem, 2.2vw, 1.8rem); margin-bottom: .6em; }
- .arch__facts { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }
- .arch__facts li { display: flex; gap: 10px; align-items: flex-start; color: var(--muted); font-size: .9rem; }
- .arch__facts i { color: var(--accent-3); margin-top: 4px; font-size: .75rem; }
- .arch__svg { width: 100%; height: auto; overflow: visible; }
- .arch__line { stroke: var(--border-strong); stroke-width: 1.2; }
- .arch__pulse { stroke: url(#agrad); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 14 200; animation: pulse 3.2s linear infinite; opacity: .9; }
- .arch__pulse--slow { animation-duration: 4.4s; animation-delay: -1s; }
- .arch__edge:nth-child(2) .arch__pulse { animation-delay: -.6s; }
- .arch__edge:nth-child(3) .arch__pulse { animation-delay: -1.2s; }
- .arch__edge:nth-child(4) .arch__pulse { animation-delay: -1.8s; }
- .arch__edge:nth-child(5) .arch__pulse { animation-delay: -2.4s; }
- .arch__edge:nth-child(6) .arch__pulse { animation-delay: -3s; }
- @keyframes pulse { from { stroke-dashoffset: 214; } to { stroke-dashoffset: -214; } }
- .arch__hub-glow { fill: rgba(var(--accent-rgb), .16); animation: hubGlow 3s ease-in-out infinite; }
- @keyframes hubGlow { 50% { fill: rgba(var(--accent-rgb), .3); } }
- .arch__hub-ring { fill: none; stroke: url(#agrad); stroke-width: 1.5; stroke-dasharray: 6 8; animation: ringSpin 30s linear infinite; transform-origin: center; transform-box: fill-box; }
- @keyframes ringSpin { to { transform: rotate(360deg); } }
- .arch__hub-fill { fill: var(--surface-solid); stroke: var(--border-strong); }
- .arch__hub-t1 { font: 700 15px var(--font-display); fill: var(--fg); }
- .arch__hub-t2 { font: 700 13px var(--font-display); fill: var(--accent); }
- .arch__hub-t3 { font: 500 8px var(--font-mono); fill: var(--dim); letter-spacing: .06em; }
- .arch__node-box { fill: var(--surface-solid); stroke: var(--border-strong); transition: stroke .3s, fill .3s; }
- .arch__node:hover .arch__node-box { stroke: var(--accent); }
- .arch__node-t { font: 600 13px var(--font-body); fill: var(--fg); }
- @media (max-width: 900px) { .arch { grid-template-columns: 1fr; } }
-</style>
@@ -60,18 +60,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
@@ -60,18 +60,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
))}
))}
</ul>
</ul>
- {limit && (
- <div class="work__more" data-reveal="scale">
- <a href="/portfolio" class="work__all-btn" data-magnetic>
- <span class="work__all-ico"><i class="fa-solid fa-layer-group"></i></span>
- <span class="work__all-text">
- <strong>See all {all.length} projects</strong>
- <small>OpenVibe, PowerChat, Amazon, side builds and more — the full case-note archive</small>
- </span>
- <span class="work__all-go" aria-hidden="true"><i class="fa-solid fa-arrow-right"></i></span>
- </a>
- </div>
- )}
</div>
</div>
</section>
</section>
@@ -129,41 +117,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
@@ -129,41 +117,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
.pcard__more { font: 600 .78rem var(--font-mono); color: var(--accent); }
.pcard__more { font: 600 .78rem var(--font-mono); color: var(--accent); }
.pcard__ext { position: relative; z-index: 3; width: 32px; height: 32px; border-radius: 50%; display: inline-grid; place-items: center; color: var(--muted); border: 1px solid var(--border); transition: all .25s; }
.pcard__ext { position: relative; z-index: 3; width: 32px; height: 32px; border-radius: 50%; display: inline-grid; place-items: center; color: var(--muted); border: 1px solid var(--border); transition: all .25s; }
.pcard__ext:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; background: rgba(var(--accent-rgb), .12); }
.pcard__ext:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; background: rgba(var(--accent-rgb), .12); }
- /* A big, unmistakable card instead of a link easy to scroll past — extra top margin and a
- fade of the section's own background above it, so there's real space to notice it in. */
- .work__more { position: relative; margin-top: clamp(-8px, -1vw, 8px); display: flex; justify-content: center; }
- .work__all-btn {
- position: relative; z-index: 1; display: flex; align-items: center; gap: 18px;
- width: min(620px, 100%); padding: 22px 26px; border-radius: 22px;
- background: var(--surface); border: 1px solid var(--border-strong);
- text-decoration: none; color: var(--fg);
- box-shadow: var(--shadow-card);
- transition: transform .4s var(--ease), border-color .4s var(--ease), box-shadow .4s var(--ease), background .4s var(--ease);
- }
- .work__all-btn:hover {
- text-decoration: none; transform: translateY(-4px) scale(1.015);
- border-color: rgba(var(--accent-rgb), .55); box-shadow: var(--shadow-card), var(--shadow-glow); background: var(--surface-2);
- }
- .work__all-ico {
- flex: none; width: 52px; height: 52px; border-radius: 16px; display: grid; place-items: center;
- background: var(--grad); color: var(--on-accent); font-size: 1.25rem; box-shadow: var(--shadow-glow);
- animation: workAllPulse 2.6s var(--ease) infinite;
- }
- @keyframes workAllPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }
- .work__all-text { flex: 1; min-width: 0; display: grid; gap: 3px; }
- .work__all-text strong { font-family: var(--font-display); font-size: 1.2rem; letter-spacing: -.01em; }
- .work__all-text small { color: var(--muted); font-size: .86rem; line-height: 1.4; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- .work__all-go {
- flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
- color: var(--accent); border: 1px solid rgba(var(--accent-rgb), .3); background: rgba(var(--accent-rgb), .08);
- transition: transform .4s var(--ease), background .3s;
- }
- .work__all-btn:hover .work__all-go { transform: translateX(5px); background: rgba(var(--accent-rgb), .16); }
- @media (max-width: 640px) {
- .work__all-btn { padding: 18px 18px; gap: 14px; }
- .work__all-text small { white-space: normal; }
- .work__all-ico { width: 44px; height: 44px; font-size: 1.05rem; }
- }
@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .pgrid { grid-template-columns: 1fr; } }
@media (max-width: 600px) { .pgrid { grid-template-columns: 1fr; } }
</style>
</style>
@@ -172,17 +125,22 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
@@ -172,17 +125,22 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
document.querySelectorAll<HTMLElement>('.work').forEach((sec) => {
document.querySelectorAll<HTMLElement>('.work').forEach((sec) => {
const btns = sec.querySelectorAll<HTMLButtonElement>('.filter');
const btns = sec.querySelectorAll<HTMLButtonElement>('.filter');
const cards = sec.querySelectorAll<HTMLElement>('.pcard');
const cards = sec.querySelectorAll<HTMLElement>('.pcard');
- btns.forEach((b) => b.addEventListener('click', () => {
- const f = b.dataset.filter!;
- btns.forEach((x) => { x.classList.toggle('is-active', x === b); x.setAttribute('aria-selected', String(x === b)); });
+ // `f` may be a comma list (the home page's group links pass e.g. "ai,amazon"); a chip is
+ // active only when it matches exactly.
+ const apply = (f: string) => {
+ const set = new Set(f.split(',').map((s) => s.trim()).filter(Boolean));
+ btns.forEach((x) => { const on = x.dataset.filter === f; x.classList.toggle('is-active', on); x.setAttribute('aria-selected', String(on)); });
let shown = 0;
let shown = 0;
cards.forEach((c) => {
cards.forEach((c) => {
- const show = f === 'all' || c.dataset.cat === f;
+ const show = set.has('all') || set.has(c.dataset.cat || '');
if (show) {
if (show) {
c.classList.remove('is-hidden');
c.classList.remove('is-hidden');
c.animate([{ opacity: 0, transform: 'translateY(14px) scale(.98)' }, { opacity: 1, transform: 'none' }], { duration: 450, delay: shown++ * 40, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' });
c.animate([{ opacity: 0, transform: 'translateY(14px) scale(.98)' }, { opacity: 1, transform: 'none' }], { duration: 450, delay: shown++ * 40, easing: 'cubic-bezier(.22,1,.36,1)', fill: 'backwards' });
} else c.classList.add('is-hidden');
} else c.classList.add('is-hidden');
});
});
- }));
+ };
+ btns.forEach((b) => b.addEventListener('click', () => { apply(b.dataset.filter!); history.replaceState(null, '', b.dataset.filter === 'all' ? location.pathname : `?cat=${b.dataset.filter}`); }));
+ const initial = new URLSearchParams(location.search).get('cat');
+ if (initial) apply(initial);
});
});
</script>
</script>
@@ -6,7 +6,7 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
@@ -6,7 +6,7 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
<section class="section skills" id="skills" aria-labelledby="skills-title">
<section class="section skills" id="skills" aria-labelledby="skills-title">
<div class="container">
<div class="container">
<div class="sec-head">
<div class="sec-head">
- <span class="sec-num" aria-hidden="true">05</span>
+ <span class="sec-num" aria-hidden="true">04</span>
<div>
<div>
<div class="sec-index">Skills</div>
<div class="sec-index">Skills</div>
<h2 class="sec-title" id="skills-title">The <em>toolbox</em>, honestly labelled.</h2>
<h2 class="sec-title" id="skills-title">The <em>toolbox</em>, honestly labelled.</h2>
@@ -9,7 +9,7 @@ const kindIcon: Record<string, string> = {
@@ -9,7 +9,7 @@ const kindIcon: Record<string, string> = {
<section class="section xp" id="experience" aria-labelledby="xp-title">
<section class="section xp" id="experience" aria-labelledby="xp-title">
<div class="container">
<div class="container">
<div class="sec-head">
<div class="sec-head">
- <span class="sec-num" aria-hidden="true">04</span>
+ <span class="sec-num" aria-hidden="true">03</span>
<div>
<div>
<div class="sec-index">Experience</div>
<div class="sec-index">Experience</div>
<h2 class="sec-title" id="xp-title">A decade of <em>owning the outcome</em>.</h2>
<h2 class="sec-title" id="xp-title">A decade of <em>owning the outcome</em>.</h2>
new file mode 100644
new file mode 100644
@@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
+---
+// The home page's single "Work" section: the OpenVibe network and the project archive in one
+// place, split into clearly labelled groups (each with its own color, header and "view all")
+// instead of a separate network section followed by an ungrouped grid. Cards are deliberately
+// compact — one line of title, two of summary — with the full case notes one click away.
+import { getCollection } from "astro:content";
+import profile from "../data/profile.json";
+
+const all = (await getCollection("projects")).sort((a, b) => a.data.order - b.data.order);
+const byCats = (cats: string[], n: number) =>
+ all.filter((p) => cats.includes(p.data.category)).sort((a, b) => Number(b.data.featured) - Number(a.data.featured) || a.data.order - b.data.order).slice(0, n);
+const countCats = (cats: string[]) => all.filter((p) => cats.includes(p.data.category)).length;
+
+const catLabel: Record<string, string> = {
+ openvibe: "OpenVibe", powerchat: "PowerChat", amazon: "Amazon", ai: "AI", infra: "Infra & ops",
+ game: "Games", tools: "Tools", python: "Python", hardware: "Hardware", web: "Web",
+};
+const netIcon: Record<string, string> = {
+ live: "fa-solid fa-tower-broadcast", network: "fa-solid fa-id-badge", media: "fa-solid fa-photo-film", tools: "fa-solid fa-toolbox",
+ games: "fa-solid fa-gamepad", community: "fa-solid fa-comments", sites: "fa-solid fa-globe",
+};
+
+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: profile.network.map((n) => ({ href: n.url, external: true, image: n.image, icon: netIcon[n.key] ?? "fa-solid fa-cube", title: n.name, sub: n.role, text: n.blurb, meta: n.stat })),
+ },
+ {
+ 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 ?? "" })),
+ },
+ {
+ 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 ?? "" })),
+ },
+ {
+ 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 ?? "" })),
+ },
+];
+---
+<section class="section work" id="work" aria-labelledby="work-title">
+ <div class="container">
+ <div class="sec-head">
+ <span class="sec-num" aria-hidden="true">02</span>
+ <div>
+ <div class="sec-index">Work</div>
+ <h2 class="sec-title" id="work-title">Things I <em>shipped</em>, grouped.</h2>
+ </div>
+ <p class="sec-lead">Four groups, short cards, real links. Every card opens the case notes; every OpenVibe tile opens the live site.</p>
+ </div>
+
+ <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}`}>
+ <header class="wk-group__head">
+ <span class="wk-group__ico"><i class={g.icon} aria-hidden="true"></i></span>
+ <div class="wk-group__txt">
+ <h3 class="wk-group__title" id={`wk-${g.key}`}>{g.title} <span class="wk-group__n mono">{g.count}</span></h3>
+ <p class="wk-group__blurb">{g.blurb}</p>
+ </div>
+ <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>
+ <ul class="wk-grid" data-stagger>
+ {g.items.map((it) => (
+ <li class={`wk-card ${it.image ? "wk-card--img" : ""}`}>
+ <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>}
+ </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>
+ </a>
+ </li>
+ ))}
+ </ul>
+ </section>
+ ))}
+ </div>
+
+ <div class="work__more" data-reveal="scale">
+ <a href="/portfolio/" class="work__all-btn" data-magnetic>
+ <span class="work__all-ico"><i class="fa-solid fa-layer-group"></i></span>
+ <span class="work__all-text">
+ <strong>See all {all.length} projects and every website</strong>
+ <small>Filter by area, open the case notes, or jump straight to the live sites</small>
+ </span>
+ <span class="work__all-go" aria-hidden="true"><i class="fa-solid fa-arrow-right"></i></span>
+ </a>
+ </div>
+ </div>
+</section>
+
+<style>
+ .wk { display: grid; gap: clamp(18px, 3vw, 30px); }
+ /* Each group carries its own tone so the eye can tell them apart mid-scroll: a colored icon
+ square, a matching left rule and a faint tinted background. */
+ .wk-group { --g: var(--accent); --g-rgb: var(--accent-rgb); position: relative; padding: clamp(16px, 2.4vw, 24px); border-radius: var(--radius-lg); background: linear-gradient(135deg, rgba(var(--g-rgb), .07), transparent 55%), var(--surface); border: 1px solid var(--border); scroll-margin-top: 90px; }
+ .wk-group::before { content: ""; position: absolute; left: -1px; top: 18%; bottom: 18%; width: 3px; border-radius: 3px; background: var(--g); box-shadow: 0 0 18px rgba(var(--g-rgb), .7); }
+ .wk-group--b { --g: var(--accent-2); --g-rgb: var(--accent-2-rgb); }
+ .wk-group--c { --g: var(--accent-3); --g-rgb: var(--accent-3-rgb); }
+ .wk-group--d { --g: var(--violet); --g-rgb: 139, 92, 246; }
+ .wk-group__head { display: grid; grid-template-columns: auto 1fr auto; gap: 14px 16px; align-items: center; margin-bottom: 16px; }
+ .wk-group__ico { width: 44px; height: 44px; border-radius: 14px; display: grid; place-items: center; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); font-size: 1.05rem; box-shadow: 0 10px 24px -14px rgba(var(--g-rgb), .9); }
+ .wk-group__txt { min-width: 0; }
+ .wk-group__title { margin: 0; font-size: clamp(1.15rem, 2vw, 1.45rem); display: flex; align-items: center; gap: 10px; }
+ .wk-group__n { font-size: .72rem; padding: 2px 8px; border-radius: 999px; background: rgba(var(--g-rgb), .14); color: var(--g); border: 1px solid rgba(var(--g-rgb), .3); }
+ .wk-group__blurb { margin: 3px 0 0; color: var(--muted); font-size: .92rem; line-height: 1.5; }
+ .wk-group__all { display: inline-flex; align-items: center; gap: 8px; padding: 9px 14px; border-radius: 999px; border: 1px solid rgba(var(--g-rgb), .35); background: rgba(var(--g-rgb), .08); color: var(--fg); font: 600 .8rem var(--font-body); text-decoration: none; white-space: nowrap; transition: background .3s var(--ease), border-color .3s var(--ease), transform .3s var(--ease); }
+ .wk-group__all i:first-child { color: var(--g); }
+ .wk-group__arrow { transition: transform .3s var(--ease); font-size: .75rem; }
+ .wk-group__all:hover { text-decoration: none; background: rgba(var(--g-rgb), .16); border-color: rgba(var(--g-rgb), .6); transform: translateY(-1px); }
+ .wk-group__all:hover .wk-group__arrow { transform: translateX(3px); }
+
+ .wk-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
+ .wk-card { min-width: 0; }
+ .wk-card__link { display: flex; flex-direction: column; height: 100%; border-radius: 16px; overflow: hidden; background: var(--surface); border: 1px solid var(--border); color: var(--fg); text-decoration: none; transition: transform .4s var(--ease), border-color .3s var(--ease), box-shadow .4s var(--ease), background .3s var(--ease); }
+ .wk-card__link:hover { text-decoration: none; transform: translateY(-3px); border-color: rgba(var(--g-rgb), .5); background: var(--surface-2); box-shadow: var(--shadow-card), 0 0 0 1px rgba(var(--g-rgb), .12), 0 16px 40px -22px rgba(var(--g-rgb), .7); }
+ .wk-card__thumb { display: block; aspect-ratio: 16 / 6; overflow: hidden; background: var(--bg-2); border-bottom: 1px solid var(--border); }
+ .wk-card__thumb img { width: 100%; height: 100%; object-fit: cover; object-position: top; transition: transform 1s var(--ease); }
+ .wk-card__link:hover .wk-card__thumb img { transform: scale(1.06); }
+ .wk-card__body { display: flex; flex-direction: column; gap: 6px; padding: 12px 13px 12px; flex: 1; }
+ .wk-card__top { display: flex; align-items: center; gap: 8px; }
+ .wk-card__ico { width: 28px; height: 28px; border-radius: 9px; display: inline-grid; place-items: center; background: rgba(var(--g-rgb), .13); color: var(--g); font-size: .78rem; flex: none; }
+ .wk-card__sub { font: 500 .66rem var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .wk-card__meta { margin-left: auto; font-size: .66rem; color: var(--dim); white-space: nowrap; }
+ .wk-card__title { font: 700 .98rem/1.25 var(--font-display); letter-spacing: -.01em; }
+ .wk-card__role { font: 500 .66rem var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--g); margin-top: -2px; }
+ .wk-card__text { color: var(--muted); font-size: .82rem; line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
+ .wk-card__go { margin-top: auto; padding-top: 4px; display: inline-flex; align-items: center; gap: 6px; font: 600 .72rem var(--font-mono); color: var(--g); }
+ .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); }
+
+ .work__more { margin-top: clamp(22px, 3vw, 34px); display: flex; justify-content: center; }
+ .work__all-btn {
+ position: relative; z-index: 1; display: flex; align-items: center; gap: 18px;
+ width: min(640px, 100%); padding: 22px 26px; border-radius: 22px;
+ background: var(--surface); border: 1px solid var(--border-strong);
+ text-decoration: none; color: var(--fg); box-shadow: var(--shadow-card);
+ transition: transform .4s var(--ease), border-color .4s var(--ease), box-shadow .4s var(--ease), background .4s var(--ease);
+ }
+ .work__all-btn:hover { text-decoration: none; transform: translateY(-4px) scale(1.015); border-color: rgba(var(--accent-rgb), .55); box-shadow: var(--shadow-card), var(--shadow-glow); background: var(--surface-2); }
+ .work__all-ico { flex: none; width: 52px; height: 52px; border-radius: 16px; display: grid; place-items: center; background: var(--grad); color: var(--on-accent); font-size: 1.25rem; box-shadow: var(--shadow-glow); animation: workAllPulse 2.6s var(--ease) infinite; }
+ @keyframes workAllPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.08); } }
+ .work__all-text { flex: 1; min-width: 0; display: grid; gap: 3px; }
+ .work__all-text strong { font-family: var(--font-display); font-size: 1.2rem; letter-spacing: -.01em; }
+ .work__all-text small { color: var(--muted); font-size: .86rem; line-height: 1.4; }
+ .work__all-go { flex: none; width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center; color: var(--accent); border: 1px solid rgba(var(--accent-rgb), .3); background: rgba(var(--accent-rgb), .08); transition: transform .4s var(--ease), background .3s; }
+ .work__all-btn:hover .work__all-go { transform: translateX(5px); background: rgba(var(--accent-rgb), .16); }
+
+ @media (max-width: 1100px) { .wk-grid { grid-template-columns: repeat(3, 1fr); } }
+ @media (max-width: 820px) { .wk-grid { grid-template-columns: repeat(2, 1fr); } .wk-group__head { grid-template-columns: auto 1fr; } .wk-group__all { grid-column: 1 / -1; justify-self: start; } }
+ @media (max-width: 480px) { .wk-grid { grid-template-columns: 1fr 1fr; gap: 8px; } .wk-card__body { padding: 10px 11px; } .wk-card__text { display: none; } .wk-card__title { font-size: .9rem; } .wk-card--img .wk-card__meta { display: none; } .work__all-btn { padding: 18px; gap: 14px; } .work__all-ico { width: 44px; height: 44px; font-size: 1.05rem; } }
+</style>
@@ -151,9 +151,9 @@
@@ -151,9 +151,9 @@
"img": "/assets/img/previews/local_portfolio_youtube-desktop-client.jpg",
"img": "/assets/img/previews/local_portfolio_youtube-desktop-client.jpg",
"label": "alexfrison.net/portfolio/youtube-desktop-client"
"label": "alexfrison.net/portfolio/youtube-desktop-client"
},
},
- "path:/#network": {
- "img": "/assets/img/previews/local_home__network.jpg",
- "label": "alexfrison.net#network"
+ "path:/#about": {
+ "img": "/assets/img/previews/local_home__about.jpg",
+ "label": "alexfrison.net#about"
},
},
"path:/#work": {
"path:/#work": {
"img": "/assets/img/previews/local_home__work.jpg",
"img": "/assets/img/previews/local_home__work.jpg",
@@ -171,6 +171,10 @@
@@ -171,6 +171,10 @@
"img": "/assets/img/previews/local_home__contact.jpg",
"img": "/assets/img/previews/local_home__contact.jpg",
"label": "alexfrison.net#contact"
"label": "alexfrison.net#contact"
},
},
+ "path:/#network": {
+ "img": "/assets/img/previews/local_home__network.jpg",
+ "label": "alexfrison.net#network"
+ },
"path:/#metrics": {
"path:/#metrics": {
"img": "/assets/img/previews/local_home__metrics.jpg",
"img": "/assets/img/previews/local_home__metrics.jpg",
"label": "alexfrison.net#metrics"
"label": "alexfrison.net#metrics"
@@ -4,8 +4,7 @@ import Hero from "../components/Hero.astro";
@@ -4,8 +4,7 @@ import Hero from "../components/Hero.astro";
import Metrics from "../components/Metrics.astro";
import Metrics from "../components/Metrics.astro";
import Marquee from "../components/Marquee.astro";
import Marquee from "../components/Marquee.astro";
import Now from "../components/Now.astro";
import Now from "../components/Now.astro";
-import Network from "../components/Network.astro";
-import ProjectGrid from "../components/ProjectGrid.astro";
+import WorkShowcase from "../components/WorkShowcase.astro";
import Timeline from "../components/Timeline.astro";
import Timeline from "../components/Timeline.astro";
import Skills from "../components/Skills.astro";
import Skills from "../components/Skills.astro";
import Contact from "../components/Contact.astro";
import Contact from "../components/Contact.astro";
@@ -15,8 +14,7 @@ import Contact from "../components/Contact.astro";
@@ -15,8 +14,7 @@ import Contact from "../components/Contact.astro";
<Metrics />
<Metrics />
<Marquee />
<Marquee />
<Now />
<Now />
- <Network />
- <ProjectGrid limit={9} />
+ <WorkShowcase />
<Timeline />
<Timeline />
<Skills />
<Skills />
<Contact />
<Contact />