openvibers/afresume Commits

Work section: bring the OpenVibe bento back as group one, no duplicated sites

OP OpenVibers committed 20 Sept 2026 · 01b5658eac · 42 files, +173 −4

The compact OpenVibe tiles are replaced by the original bento (big Live
tile, four small, two wide) plus the "how it fits together" card and
diagram, now living inside the Work section's OpenVibe group
(OpenVibeBento.astro). OpenVibe case-study projects (openvibe-* slugs)
are excluded from the other groups, so each site appears exactly once
on the home page and there is no separate network section or nav item.

Changes

ddiff --git a/public/assets/img/previews/alexfrison.net.jpg b/public/assets/img/previews/alexfrison.net.jpg
iindex 6cc0f0e..02f0e7f 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 5f9f842..db19f30 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
iindex 805bc1d..89dddba 100644
BBinary files a/public/assets/img/previews/local_home__about.jpg 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 d0d1fd2..4510143 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 3203e25..863f615 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 6b4c165..8c0ece0 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 b64df1c..45d1f27 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 60853e5..4c28936 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 d1d2bd8..6138ef1 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 69fb7c9..a2f896f 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 86c4d2e..273e04d 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_gameserverlists.jpg b/public/assets/img/previews/local_portfolio_gameserverlists.jpg
iindex 6769259..1347486 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 e03a890..07359fb 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 62501d1..7426a1d 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 2c483cf..7fe2714 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 b6cf955..3502749 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 b358388..6a18ce0 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 d9768cc..45049a0 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 e8cb46e..f32b23b 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 6d17196..ed8e129 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 981147b..dabf2bb 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 45a5916..f625359 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 7297f0d..98cac6b 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 8af4a26..843ea32 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 4124928..1297efe 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 c4007d9..aac4645 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-tools.jpg b/public/assets/img/previews/local_portfolio_openvibe-tools.jpg
iindex 06df904..db60164 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 373623f..5b166d5 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 b420b1f..b11b413 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 f1bf02d..57a42cc 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_robotstreamer-toolkit.jpg b/public/assets/img/previews/local_portfolio_robotstreamer-toolkit.jpg
iindex 87f2eef..71e679e 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 4e31d79..34cb67c 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 db471ac..7e353e1 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 cc21fb2..c51310c 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 70bce89..8088ab6 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.games.jpg b/public/assets/img/previews/openvibe.games.jpg
iindex 9684935..a2ff4c7 100644
BBinary files a/public/assets/img/previews/openvibe.games.jpg and b/public/assets/img/previews/openvibe.games.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.live.jpg b/public/assets/img/previews/openvibe.live.jpg
iindex 098f9b4..5801296 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 4dd5d2d..7a82c4e 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 841312f..2105314 100644
BBinary files a/public/assets/img/previews/openvibe.network.jpg and b/public/assets/img/previews/openvibe.network.jpg differ
ddiff --git a/public/assets/img/previews/openvibe.tools.jpg b/public/assets/img/previews/openvibe.tools.jpg
iindex 554a0e5..a870f28 100644
BBinary files a/public/assets/img/previews/openvibe.tools.jpg and b/public/assets/img/previews/openvibe.tools.jpg differ
ddiff --git a/src/components/OpenVibeBento.astro b/src/components/OpenVibeBento.astro
nnew file mode 100644
iindex 0000000..a7d6958
---- /dev/null
++++ b/src/components/OpenVibeBento.astro
@@@ -0,0 +1,165 @@
++---
++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"],
++];
++---
++<!-- The OpenVibe network as the bento it used to be (one big Live tile, four small tiles, two wide
++ ones) plus the "how it fits together" card and diagram. Rendered inside the Work section's
++ OpenVibe group, so the sites appear once on the page instead of as tiles here and again as
++ project cards below. -->
++<div class="ovb">
++ <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>
++
++<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/WorkShowcase.astro b/src/components/WorkShowcase.astro
iindex 4cd419d..97c2ea0 100644
---- a/src/components/WorkShowcase.astro
++++ b/src/components/WorkShowcase.astro
@@@ -5,11 +5,14 @@
// 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";
++import OpenVibeBento from "./OpenVibeBento.astro";
const all = (await getCollection("projects")).sort((a, b) => a.data.order - b.data.order);
++// OpenVibe's own services are shown once, as the bento in group one — their case-study
++// pages (openvibe-* slugs) stay out of the other groups so nothing appears twice.
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;
++ all.filter((p) => cats.includes(p.data.category) && !p.slug.startsWith("openvibe-")).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) && !p.slug.startsWith("openvibe-")).length;
const catLabel: Record<string, string> = {
openvibe: "OpenVibe", powerchat: "PowerChat", amazon: "Amazon", ai: "AI", infra: "Infra & ops",
@@@ -25,7 +28,7 @@ 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 })),
++ items: [] as { href: string; external: boolean; image?: string | null; icon: string; title: string; sub: string; text: string; meta: string }[],
},
{
key: "powerchat", tone: "b", icon: "fa-solid fa-bolt",
@@@ -69,7 +72,8 @@ const groups = [
</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.key === "openvibe" && <OpenVibeBento />}
++ <ul class="wk-grid" data-stagger hidden={g.items.length === 0}>
{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}>