openvibers/afresume Commits

Home: merge the OpenVibe network section into a grouped Work showcase; nav gets Profile; portfolio filter reads ?cat=

OP OpenVibers committed 20 Sept 2026 · 62b08d373c · 50 files, +193 −235

- New WorkShowcase.astro replaces Network.astro + the limited ProjectGrid
  on the home page: four color-coded groups (OpenVibe network, PowerChat,
  AI/data/automation, games/tools/side projects), each with an icon
  header, count, short blurb, compact two-line cards and a "view all"
  link into the filtered portfolio; one big "see everything" button at
  the end. The OpenVibe group keeps id="network" so old links still land.
- ProjectGrid: removed the home-only limit/fade/more-button code; the
  filter now accepts ?cat=a,b and reflects the active chip in the URL.
- Nav: "OpenVibe" item becomes "Profile" (#about); hero CTA points at
  #work; section numbers renumbered; link previews regenerated.

Changes

ddiff --git a/public/assets/img/previews/alexfrison.net.jpg b/public/assets/img/previews/alexfrison.net.jpg
iindex 9324f99..6cc0f0e 100644
BBinary files a/public/assets/img/previews/alexfrison.net.jpg and b/public/assets/img/previews/alexfrison.net.jpg differ
ddiff --git a/public/assets/img/previews/local_home.jpg b/public/assets/img/previews/local_home.jpg
iindex 4297537..5f9f842 100644
BBinary files a/public/assets/img/previews/local_home.jpg and b/public/assets/img/previews/local_home.jpg differ
ddiff --git a/public/assets/img/previews/local_home__about.jpg b/public/assets/img/previews/local_home__about.jpg
nnew file mode 100644
iindex 0000000..805bc1d
BBinary files /dev/null and b/public/assets/img/previews/local_home__about.jpg differ
ddiff --git a/public/assets/img/previews/local_home__contact.jpg b/public/assets/img/previews/local_home__contact.jpg
iindex d8c76da..d0d1fd2 100644
BBinary files a/public/assets/img/previews/local_home__contact.jpg and b/public/assets/img/previews/local_home__contact.jpg differ
ddiff --git a/public/assets/img/previews/local_home__experience.jpg b/public/assets/img/previews/local_home__experience.jpg
iindex 33e8325..3203e25 100644
BBinary files a/public/assets/img/previews/local_home__experience.jpg and b/public/assets/img/previews/local_home__experience.jpg differ
ddiff --git a/public/assets/img/previews/local_home__metrics.jpg b/public/assets/img/previews/local_home__metrics.jpg
iindex 1a96380..6b4c165 100644
BBinary files a/public/assets/img/previews/local_home__metrics.jpg and b/public/assets/img/previews/local_home__metrics.jpg differ
ddiff --git a/public/assets/img/previews/local_home__network.jpg b/public/assets/img/previews/local_home__network.jpg
iindex af4322b..b64df1c 100644
BBinary files a/public/assets/img/previews/local_home__network.jpg and b/public/assets/img/previews/local_home__network.jpg differ
ddiff --git a/public/assets/img/previews/local_home__skills.jpg b/public/assets/img/previews/local_home__skills.jpg
iindex e828cf0..60853e5 100644
BBinary files a/public/assets/img/previews/local_home__skills.jpg and b/public/assets/img/previews/local_home__skills.jpg differ
ddiff --git a/public/assets/img/previews/local_home__work.jpg b/public/assets/img/previews/local_home__work.jpg
iindex c2592fe..d1d2bd8 100644
BBinary files a/public/assets/img/previews/local_home__work.jpg and b/public/assets/img/previews/local_home__work.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio.jpg b/public/assets/img/previews/local_portfolio.jpg
iindex afdc457..69fb7c9 100644
BBinary files a/public/assets/img/previews/local_portfolio.jpg and b/public/assets/img/previews/local_portfolio.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg b/public/assets/img/previews/local_portfolio_amazon-sql-copilot.jpg
iindex e4c56b1..86c4d2e 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg b/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg
iindex edbe8bf..b495390 100644
BBinary files a/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg and b/public/assets/img/previews/local_portfolio_amazon-userscripts.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_gameserverlists.jpg b/public/assets/img/previews/local_portfolio_gameserverlists.jpg
iindex 6b01724..6769259 100644
BBinary files a/public/assets/img/previews/local_portfolio_gameserverlists.jpg and b/public/assets/img/previews/local_portfolio_gameserverlists.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_gameserverstats.jpg b/public/assets/img/previews/local_portfolio_gameserverstats.jpg
iindex 62feb85..e03a890 100644
BBinary files a/public/assets/img/previews/local_portfolio_gameserverstats.jpg and b/public/assets/img/previews/local_portfolio_gameserverstats.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_hartools.jpg b/public/assets/img/previews/local_portfolio_hartools.jpg
iindex acf6795..62501d1 100644
BBinary files a/public/assets/img/previews/local_portfolio_hartools.jpg and b/public/assets/img/previews/local_portfolio_hartools.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg b/public/assets/img/previews/local_portfolio_inbound-pull-tool.jpg
iindex 5974a6c..2c483cf 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg b/public/assets/img/previews/local_portfolio_irdr-autonomous-bot.jpg
iindex 452e359..b6cf955 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg b/public/assets/img/previews/local_portfolio_irdr-quality-recap.jpg
iindex b6b2402..b358388 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_portfolio_live-translation.jpg b/public/assets/img/previews/local_portfolio_live-translation.jpg
iindex 699d9a3..d9768cc 100644
BBinary files a/public/assets/img/previews/local_portfolio_live-translation.jpg and b/public/assets/img/previews/local_portfolio_live-translation.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg b/public/assets/img/previews/local_portfolio_lyric-video-toolkit.jpg
iindex 26a92ea..e8cb46e 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_portfolio_mp-select-mini.jpg b/public/assets/img/previews/local_portfolio_mp-select-mini.jpg
iindex 61eef08..6d17196 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-community.jpg b/public/assets/img/previews/local_portfolio_openvibe-community.jpg
iindex 51bd989..981147b 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-community.jpg and b/public/assets/img/previews/local_portfolio_openvibe-community.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg b/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg
iindex 35e811e..45a5916 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg and b/public/assets/img/previews/local_portfolio_openvibe-cutover.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-games.jpg b/public/assets/img/previews/local_portfolio_openvibe-games.jpg
iindex 6f6ff2a..7297f0d 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-games.jpg and b/public/assets/img/previews/local_portfolio_openvibe-games.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-live.jpg b/public/assets/img/previews/local_portfolio_openvibe-live.jpg
iindex 1c44142..8af4a26 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-live.jpg and b/public/assets/img/previews/local_portfolio_openvibe-live.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-media.jpg b/public/assets/img/previews/local_portfolio_openvibe-media.jpg
iindex 3afe48f..4124928 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-media.jpg and b/public/assets/img/previews/local_portfolio_openvibe-media.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-network.jpg b/public/assets/img/previews/local_portfolio_openvibe-network.jpg
iindex 25314b8..c4007d9 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-network.jpg and b/public/assets/img/previews/local_portfolio_openvibe-network.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-sites.jpg b/public/assets/img/previews/local_portfolio_openvibe-sites.jpg
iindex d2bf4e4..f857cd4 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-sites.jpg and b/public/assets/img/previews/local_portfolio_openvibe-sites.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_openvibe-tools.jpg b/public/assets/img/previews/local_portfolio_openvibe-tools.jpg
iindex 7e93250..06df904 100644
BBinary files a/public/assets/img/previews/local_portfolio_openvibe-tools.jpg and b/public/assets/img/previews/local_portfolio_openvibe-tools.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_perf-harness.jpg b/public/assets/img/previews/local_portfolio_perf-harness.jpg
iindex ba9b844..373623f 100644
BBinary files a/public/assets/img/previews/local_portfolio_perf-harness.jpg and b/public/assets/img/previews/local_portfolio_perf-harness.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg b/public/assets/img/previews/local_portfolio_powerchat-developer-api.jpg
iindex a8dcaa0..b420b1f 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_portfolio_powerchat-payments.jpg b/public/assets/img/previews/local_portfolio_powerchat-payments.jpg
iindex 4ed0263..f1bf02d 100644
BBinary files a/public/assets/img/previews/local_portfolio_powerchat-payments.jpg and b/public/assets/img/previews/local_portfolio_powerchat-payments.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_pytools-workshop.jpg b/public/assets/img/previews/local_portfolio_pytools-workshop.jpg
iindex 2a90698..48b9adc 100644
BBinary files a/public/assets/img/previews/local_portfolio_pytools-workshop.jpg and b/public/assets/img/previews/local_portfolio_pytools-workshop.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg
iindex 073dd66..87f2eef 100644
BBinary files a/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg and b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg differ
ddiff --git a/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg b/public/assets/img/previews/local_portfolio_youtube-desktop-client.jpg
iindex fcf9b49..4e31d79 100644
BBinary 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
ddiff --git a/public/assets/img/previews/local_resume.jpg b/public/assets/img/previews/local_resume.jpg
iindex 12ef07d..db471ac 100644
BBinary files a/public/assets/img/previews/local_resume.jpg and b/public/assets/img/previews/local_resume.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.chat.jpg b/public/assets/img/previews/openvibe.chat.jpg
iindex dc4d1fd..cc21fb2 100644
BBinary files a/public/assets/img/previews/openvibe.chat.jpg and b/public/assets/img/previews/openvibe.chat.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.community.jpg b/public/assets/img/previews/openvibe.community.jpg
iindex 9c6d950..70bce89 100644
BBinary files a/public/assets/img/previews/openvibe.community.jpg and b/public/assets/img/previews/openvibe.community.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.live.jpg b/public/assets/img/previews/openvibe.live.jpg
iindex 97c55b1..098f9b4 100644
BBinary files a/public/assets/img/previews/openvibe.live.jpg and b/public/assets/img/previews/openvibe.live.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.media.jpg b/public/assets/img/previews/openvibe.media.jpg
iindex da0d391..4dd5d2d 100644
BBinary files a/public/assets/img/previews/openvibe.media.jpg and b/public/assets/img/previews/openvibe.media.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.network.jpg b/public/assets/img/previews/openvibe.network.jpg
iindex 76599b8..841312f 100644
BBinary files a/public/assets/img/previews/openvibe.network.jpg and b/public/assets/img/previews/openvibe.network.jpg differ
ddiff --git a/src/components/Hero.astro b/src/components/Hero.astro
iindex 2ee023a..694cee1 100644
---- a/src/components/Hero.astro
++++ b/src/components/Hero.astro
@@@ -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>
ddiff --git a/src/components/Nav.astro b/src/components/Nav.astro
iindex 317ab8e..25cc6fa 100644
---- a/src/components/Nav.astro
++++ b/src/components/Nav.astro
@@@ -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" },
ddiff --git a/src/components/Network.astro b/src/components/Network.astro
ddeleted file mode 100644
iindex f0901ea..0000000
---- a/src/components/Network.astro
++++ /dev/null
@@@ -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>
ddiff --git a/src/components/ProjectGrid.astro b/src/components/ProjectGrid.astro
iindex b70c8b0..dfbadcf 100644
---- a/src/components/ProjectGrid.astro
++++ b/src/components/ProjectGrid.astro
@@@ -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>
ddiff --git a/src/components/Skills.astro b/src/components/Skills.astro
iindex dc559cc..84abfb8 100644
---- a/src/components/Skills.astro
++++ b/src/components/Skills.astro
@@@ -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>
ddiff --git a/src/components/Timeline.astro b/src/components/Timeline.astro
iindex 0457c38..6b28030 100644
---- a/src/components/Timeline.astro
++++ b/src/components/Timeline.astro
@@@ -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>
ddiff --git a/src/components/WorkShowcase.astro b/src/components/WorkShowcase.astro
nnew file mode 100644
iindex 0000000..4cd419d
---- /dev/null
++++ b/src/components/WorkShowcase.astro
@@@ -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>
ddiff --git a/src/data/link-previews.json b/src/data/link-previews.json
iindex ee40606..be76a76 100644
---- a/src/data/link-previews.json
++++ b/src/data/link-previews.json
@@@ -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"
ddiff --git a/src/pages/index.astro b/src/pages/index.astro
iindex d328e37..50175df 100644
---- a/src/pages/index.astro
++++ b/src/pages/index.astro
@@@ -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 />