openvibers/afresume Commits

Layout audit: wasted space removed on every page, compact OpenVibe group, steadier phones

OP OpenVibers committed 26 Sept 2026 · 6e5b563dce · 12 files, +108 −38

From screenshots of every page at phone and desktop width:
- Sections sit closer (36-88px padding instead of 52-116px): 85-105px between sections on phones
  instead of 117-137px, about 190px on desktop instead of 230px.
- Profile: the five strengths are their own row under the pitch and the side cards, so the two
  columns end together on desktop; on desktop their icon sits above the title; the two buttons are
  an even pair on phones.
- Work: each group's accent rule sits beside its title only (it ran the length of the group); card
  grids fill their row (the PowerChat pair no longer leaves half a row empty); the group count stays
  with the title.
- OpenVibe group on phones: about half the height (2300px -> 1165px). Live across the top, the other
  six two by two with icon, short name ("Live", not "OpenVibe.Live") and stat; the explainer folds
  behind "Read more" (open without JavaScript and on wider screens); the unreadable diagram is gone
  at that width; the "How it fits together" label no longer touches its title.
- Skills: masonry columns, so shorter cards no longer stretch to their row with a blank bottom.
- Contact: lighter padding; its actions stack full width on phones. Résumé actions likewise.
- Portfolio on phones: cards without a screenshot drop the icon-only placeholder frame and
  screenshots are shorter (the page is 8262px instead of 11237px).
- Project pages: the role reads on one line; less padding around the text on phones.
- Experience on phones: each job's icon node sits on the card's edge line beside the title.
- Buttons, bento tiles and project cards no longer keep a permanent GPU layer (will-change), and
  cards drop the backdrop blur on phones: phones were painting some of those layers blank (the
  missing Profile buttons).
- Primary buttons use white text unless the accent is genuinely light.
- Phones: the background grid is clearly visible and the film grain is a slower, lighter texture, so
  it no longer reads as the screen glitching.

Changes

ddiff --git a/src/components/Contact.astro b/src/components/Contact.astro
iindex 40c9a74..d2321c1 100644
---- a/src/components/Contact.astro
++++ b/src/components/Contact.astro
@@@ -28,7 +28,8 @@ import profile from "../data/profile.json";
</section>
<style>
-- .ct { overflow: hidden; padding: clamp(28px, 5vw, 60px); }
++ .ct { overflow: hidden; padding: clamp(20px, 4vw, 48px); }
++ @media (max-width: 560px) { .ct__actions { display: grid; grid-template-columns: 1fr; } .ct__actions .btn { justify-content: center; } }
.ct__glow { position: absolute; inset: -40% -20% auto auto; width: 60%; aspect-ratio: 1; border-radius: 50%; background: radial-gradient(closest-side, rgba(var(--accent-rgb), .35), transparent 70%); filter: blur(30px); animation: breathe 9s ease-in-out infinite; pointer-events: none; }
@keyframes breathe { 50% { transform: scale(1.15) translate(-6%, 8%); opacity: .7; } }
.ct__grid { position: relative; display: grid; grid-template-columns: 1.3fr 1fr; gap: clamp(24px, 4vw, 56px); align-items: center; }
ddiff --git a/src/components/Now.astro b/src/components/Now.astro
iindex 4c0066d..b88c952 100644
---- a/src/components/Now.astro
++++ b/src/components/Now.astro
@@@ -24,11 +24,6 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
<div class="now__grid">
<article class="card spot now__pitch" data-reveal>
<p class="now__text">{pitchParts.map((p) => typeof p === "string" ? p : <a href={p.href} target={p.external ? "_blank" : undefined} rel={p.external ? "noopener" : undefined}>{p.text}</a>)}</p>
-- <ul class="now__hl" aria-label="What I bring">
-- {profile.highlights.map((h) => (
-- <li><i class={h.icon} aria-hidden="true"></i><div><strong>{h.title}</strong><span>{h.text}</span></div></li>
-- ))}
-- </ul>
<p class="now__text">Ten years of running my own company taught me to care about the whole loop: the code, the servers, the invoice and the person on the other end. I like small teams, hard problems and software that stays up.</p>
<div class="now__cta">
<a href="#experience" class="btn btn--ghost">Experience <span class="btn__arrow">→</span></a>
@@@ -36,6 +31,12 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
</div>
</article>
++ <ul class="now__hl" aria-label="What I bring" data-stagger>
++ {profile.highlights.map((h) => (
++ <li><i class={h.icon} aria-hidden="true"></i><div><strong>{h.title}</strong><span>{h.text}</span></div></li>
++ ))}
++ </ul>
++
<aside class="now__side">
<div class="card spot now__status" data-reveal data-delay="100">
<div class="now__status-head"><span class="live-dot"></span><span class="eyebrow">Right now</span></div>
@@@ -61,17 +62,22 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
</section>
<style>
-- .now__grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 18px; align-items: start; }
++ /* The pitch and the side cards share the first row; the five strengths run under both as their own
++ row, so neither column ends far above the other. Phones: pitch, strengths, then the side cards. */
++ .now__grid { display: grid; grid-template-columns: 1.35fr 1fr; grid-template-areas: "pitch side" "hl hl"; gap: 18px; align-items: start; }
++ .now__pitch { grid-area: pitch; } .now__side { grid-area: side; } .now__hl { grid-area: hl; }
.now__pitch { padding: clamp(24px, 3.5vw, 40px); }
.now__text { font-size: 1.08rem; line-height: 1.8; color: var(--muted); }
.now__text:first-child { color: var(--fg); font-size: clamp(1.1rem, 1.6vw, 1.3rem); font-weight: 500; line-height: 1.6; letter-spacing: -.005em; }
-- .now__hl { list-style: none; margin: 22px 0 18px; padding: 0; display: grid; gap: 10px; }
++ .now__hl { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; }
.now__hl li { display: flex; gap: 14px; align-items: flex-start; padding: 12px 14px; border-radius: 14px; background: var(--surface); border: 1px solid var(--border); transition: border-color .3s var(--ease), transform .3s var(--ease); }
++ @media (min-width: 901px) { .now__hl li { flex-direction: column; gap: 10px; } }
.now__hl li:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateX(4px); }
.now__hl i { flex: none; width: 36px; height: 36px; border-radius: 10px; display: inline-grid; place-items: center; background: rgba(var(--accent-rgb), .12); color: var(--accent); font-size: .95rem; }
.now__hl strong { display: block; color: var(--fg); font-size: .98rem; margin-bottom: 2px; }
.now__hl span { color: var(--muted); font-size: .9rem; line-height: 1.55; }
-- .now__cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
++ .now__cta { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
++ @media (max-width: 560px) { .now__cta { display: grid; grid-template-columns: 1fr 1fr; } .now__cta .btn { justify-content: center; padding: 12px 14px; } .now__pitch { padding: 20px; } }
.now__side { display: grid; gap: 18px; }
.now__status-head { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; }
.now__status-list, .now__seek { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }
@@@ -86,5 +92,5 @@ const statusAlsoParts = linkify(profile.status.also, LINK_TERMS);
.now__seek li:first-child { border-top: 0; padding-top: 0; }
.now__seek strong { color: var(--fg); font-size: .95rem; }
.now__seek span { color: var(--muted); font-size: .86rem; }
-- @media (max-width: 900px) { .now__grid { grid-template-columns: 1fr; } }
++ @media (max-width: 900px) { .now__grid { grid-template-columns: 1fr; grid-template-areas: "pitch" "hl" "side"; } }
</style>
ddiff --git a/src/components/OpenVibeBento.astro b/src/components/OpenVibeBento.astro
iindex a7d6958..de68b4d 100644
---- a/src/components/OpenVibeBento.astro
++++ b/src/components/OpenVibeBento.astro
@@@ -7,6 +7,8 @@ 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",
};
++// "OpenVibe.Live" -> "OpenVibe." + "Live": phones show only the short name (the group already says OpenVibe).
++const short = (n: string) => n.startsWith("OpenVibe.") ? n.slice(9) : null;
// Architecture diagram geometry (viewBox 800 x 420)
const hub = { x: 400, y: 210 };
const nodes = [
@@@ -34,7 +36,7 @@ const edges = [
<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>
++ <h3 class="tile__name">{short(big.name) ? <><span class="tile__pre">OpenVibe.</span>{short(big.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>
@@@ -48,7 +50,7 @@ const edges = [
<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>
++ <h3 class="tile__name">{short(s.name) ? <><span class="tile__pre">OpenVibe.</span>{short(s.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>
@@@ -60,7 +62,10 @@ const edges = [
<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>
++ <details class="arch__more" open>
++ <summary>Read more</summary>
<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>
++ </details>
<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>
@@@ -107,7 +112,6 @@ const edges = [
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; }
@@@ -131,11 +135,14 @@ const edges = [
.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; } }
++ /* The diagram is unreadable at phone width; the facts beside it say the same thing. */
++ @media (max-width: 560px) { .arch__svg { display: none; } .arch { padding: 18px; } }
@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 .eyebrow { display: inline-flex; align-items: center; gap: 8px; margin-bottom: 10px; }
++ .arch__copy h3 { margin-top: 0; }
.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; }
@@@ -162,4 +169,36 @@ const edges = [
.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; } }
++
++ /* Phones: the network as a compact grid (Live across the top, the other six two by two: icon,
++ short name, stat); the explainer paragraph folds behind "Read more". */
++ .arch__more summary { display: none; }
++ @media (max-width: 560px) {
++ .bento { grid-template-columns: 1fr 1fr; grid-auto-rows: auto; gap: 8px; }
++ .tile, .tile--big, .tile--community, .tile--sites { grid-column: span 1; min-height: 0; }
++ .tile--big { grid-column: 1 / -1; }
++ .tile__pre { display: none; }
++ .tile__body { padding: 12px 12px 11px; gap: 4px; }
++ .tile__top { margin-bottom: 2px; gap: 8px; }
++ .tile__icon { width: 28px; height: 28px; border-radius: 8px; font-size: .78rem; }
++ .tile__role { font-size: .6rem; letter-spacing: .08em; }
++ .tile__name, .tile--big .tile__name { font-size: 1.05rem; }
++ .tile--big .tile__name { font-size: 1.35rem; }
++ .tile__blurb { display: none; }
++ .tile--big .tile__blurb { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; font-size: .85rem; }
++ .tile__foot { margin-top: 2px; }
++ .tile__stat { white-space: normal; line-height: 1.35; }
++ .tile__go { display: none; }
++ .tile__shot img { opacity: .18; }
++ .arch__more summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; margin: 4px 0 10px; font: 600 .82rem var(--font-mono); color: var(--accent); }
++ .arch__more summary::-webkit-details-marker { display: none; }
++ .arch__more summary::after { content: "▾"; transition: transform .2s; }
++ .arch__more[open] summary::after { transform: rotate(180deg); }
++ .arch__more[open] summary { margin-bottom: 4px; }
++ }
</style>
++
++<script>
++ // Phones start with the architecture explainer folded; wider screens (and no-JS) show it open.
++ if (matchMedia('(max-width: 560px)').matches) document.querySelectorAll<HTMLDetailsElement>('.arch__more').forEach((d) => { d.open = false; });
++</script>
ddiff --git a/src/components/ProjectGrid.astro b/src/components/ProjectGrid.astro
iindex 39a19da..21c972a 100644
---- a/src/components/ProjectGrid.astro
++++ b/src/components/ProjectGrid.astro
@@@ -86,7 +86,6 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
position: relative; overflow: hidden; display: flex; flex-direction: column;
border-radius: var(--radius-lg); background: var(--surface); border: 1px solid var(--border);
transition: transform .5s var(--ease), border-color .35s var(--ease), box-shadow .5s var(--ease), opacity .4s var(--ease);
-- will-change: transform;
}
.pcard:hover { border-color: rgba(var(--accent-rgb), .4); box-shadow: var(--shadow-card), var(--shadow-glow); }
.pcard.is-hidden { display: none; }
@@@ -120,6 +119,9 @@ const cats = Array.from(new Set(projects.map((p) => p.data.category)));
.pcard__ext:hover { color: var(--fg); border-color: var(--accent); text-decoration: none; background: rgba(var(--accent-rgb), .12); }
@media (max-width: 1000px) { .pgrid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .pgrid { grid-template-columns: 1fr; } }
++ /* Phones: a card without a screenshot drops the icon-only placeholder (its icon is already in the
++ card's top row), and screenshots are shorter, so the list is not mostly empty frames. */
++ @media (max-width: 620px) { .pcard__deco { display: none; } .pcard__img { aspect-ratio: 16 / 8; } }
</style>
<script>
ddiff --git a/src/components/Skills.astro b/src/components/Skills.astro
iindex 84abfb8..195205c 100644
---- a/src/components/Skills.astro
++++ b/src/components/Skills.astro
@@@ -41,8 +41,9 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
</section>
<style>
-- .sk-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
-- .sk { padding: 22px; }
++ /* Columns, not a grid: cards of different lengths stack without blank space under the shorter ones. */
++ .sk-grid { columns: 3; column-gap: 14px; }
++ .sk { padding: 22px; break-inside: avoid; margin-bottom: 14px; }
.sk__head { display: flex; gap: 14px; align-items: flex-start; margin-bottom: 14px; }
.sk__icon { width: 40px; height: 40px; flex: none; border-radius: 12px; display: inline-grid; place-items: center; background: var(--grad-soft); color: var(--fg); border: 1px solid var(--border); font-size: .95rem; }
.sk__title { margin: 0 0 3px; font-size: 1.05rem; }
@@@ -55,7 +56,6 @@ const lvl = { core: 3, strong: 2, familiar: 1 } as const;
.sk__bars i { width: 12px; height: 5px; border-radius: 3px; background: var(--surface-2); border: 1px solid var(--border); transition: background .3s, box-shadow .3s; }
.sk__bars i.on { background: var(--accent); border-color: transparent; box-shadow: 0 0 8px rgba(var(--accent-rgb), .5); }
.sk__item:hover .sk__bars i.on { background: var(--accent-2); box-shadow: 0 0 8px rgba(var(--accent-2-rgb), .5); }
-- @media (min-width: 1001px) { .sk:last-child { grid-column: 1 / -1; } .sk:last-child .sk__list { grid-template-columns: repeat(3, 1fr); column-gap: 28px; } .sk:last-child .sk__item:nth-child(-n+3) { border-top: 0; } }
-- @media (max-width: 1000px) { .sk-grid { grid-template-columns: repeat(2, 1fr); } }
-- @media (max-width: 620px) { .sk-grid { grid-template-columns: 1fr; } }
++ @media (max-width: 1000px) { .sk-grid { columns: 2; } }
++ @media (max-width: 620px) { .sk-grid { columns: 1; } .sk { padding: 18px; } }
</style>
ddiff --git a/src/components/Timeline.astro b/src/components/Timeline.astro
iindex 355dab9..ddb1a1e 100644
---- a/src/components/Timeline.astro
++++ b/src/components/Timeline.astro
@@@ -30,7 +30,6 @@ const kindIcon: Record<string, string> = {
<article class="tl__card card spot">
<header class="tl__head">
<div class="tl__when mono">
-- <i class={`${kindIcon[e.data.kind]} tl__kind`} aria-hidden="true"></i>
{e.data.current && <span class="live-dot"></span>}
<span>{e.data.start} — {e.data.end}</span>
</div>
@@@ -95,12 +94,13 @@ const kindIcon: Record<string, string> = {
.tl__item.is-featured .tl__card { padding: clamp(24px, 3vw, 34px); }
}
.tl__aside { display: none; }
-- .tl__kind { display: none; }
-- /* Phones and tablets: no separate rail or nodes eating the left edge. The card uses the full width,
-- the progress line is built into its left side (filling as you scroll through it) and the job-type
-- icon sits with the dates. */
++ /* Phones and tablets: no separate rail eating the left edge. The card uses the full width, the
++ progress line is built into its left side (filling as you scroll through it), and each job's icon
++ node sits on that line beside the title, half in the page gutter, so it costs the text no width. */
@media (max-width: 899px) {
-- .tl__rail, .tl__node { display: none; }
++ .tl__rail { display: none; }
++ .tl__node { left: -14px; top: 22px; width: 32px; height: 32px; font-size: .72rem; z-index: 1; }
++ .tl__head { padding-left: 18px; }
.tl__item { padding-left: 0; }
.tl__card { overflow: hidden; }
.tl__card::after {
@@@ -108,7 +108,6 @@ const kindIcon: Record<string, string> = {
background: linear-gradient(180deg, var(--accent), var(--accent-2)) top / 100% calc(var(--p, 0) * 100%) no-repeat, var(--border-strong);
}
.tl__item.is-current .tl__card::after { box-shadow: 0 0 12px rgba(var(--accent-rgb), .45); }
-- .tl__kind { display: inline-grid; place-items: center; width: 24px; height: 24px; border-radius: 7px; font-size: .68rem; letter-spacing: 0; background: rgba(var(--accent-rgb), .12); color: var(--accent); }
}
@media (min-width: 900px) { .tl__aside { display: grid; } }
.tl__aside-when { font-family: var(--font-mono); font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; color: var(--accent); display: inline-flex; align-items: center; gap: 10px; }
ddiff --git a/src/components/WorkShowcase.astro b/src/components/WorkShowcase.astro
iindex 3fe56c6..c0fab56 100644
---- a/src/components/WorkShowcase.astro
++++ b/src/components/WorkShowcase.astro
@@@ -125,14 +125,17 @@ const groups = [
/* 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); }
++ /* The group's accent rule sits on its left edge beside the title only (not the length of the group). */
++ .wk-group__ico { position: relative; }
++ .wk-group__ico::before { content: ""; position: absolute; left: calc(-1 * clamp(16px, 2.4vw, 24px) - 2px); top: -2px; bottom: -2px; 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__title { margin: 0; font-size: clamp(1.15rem, 2vw, 1.45rem); }
++ .wk-group__title .wk-group__n { display: inline-block; margin-left: 8px; vertical-align: .15em; line-height: 1.4; }
.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); }
@@@ -141,7 +144,7 @@ const groups = [
.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-grid { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 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); }
@@@ -188,7 +191,6 @@ const groups = [
.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: 820px) { .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/layouts/Base.astro b/src/layouts/Base.astro
iindex 637087b..9de336d 100644
---- a/src/layouts/Base.astro
++++ b/src/layouts/Base.astro
@@@ -153,7 +153,7 @@ const imageUrl = new URL(image, site).toString();
// Text on top of the accent: relative luminance decides dark ink vs white.
var rgb = hexToRgbStr(accent).split(', ').map(function (v) { v = parseInt(v, 10) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); });
var lum = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
-- root.setProperty('--on-accent', lum > 0.36 ? '#0b0f1a' : '#ffffff');
++ root.setProperty('--on-accent', lum > 0.5 ? '#0b0f1a' : '#ffffff');
root.setProperty('--accent', accent);
root.setProperty('--accent-rgb', hexToRgbStr(accent));
root.setProperty('--violet', violet);
ddiff --git a/src/pages/portfolio/[slug].astro b/src/pages/portfolio/[slug].astro
iindex c81f077..ae188de 100644
---- a/src/pages/portfolio/[slug].astro
++++ b/src/pages/portfolio/[slug].astro
@@@ -85,8 +85,8 @@ const catLabel: Record<string, string> = {
.pp__soon { display: inline-flex; align-items: center; gap: 6px; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; padding: 4px 10px; border-radius: 999px; color: var(--accent-3); background: rgba(var(--accent-3-rgb), .12); border: 1px solid rgba(var(--accent-3-rgb), .35); }
.pp__title { font-size: clamp(2.2rem, 5.5vw, 4rem); margin: 0 0 .3em; }
.pp__lead { font-size: clamp(1.05rem, 1.6vw, 1.3rem); color: var(--fg); max-width: 62ch; line-height: 1.6; }
-- .pp__meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 14px; margin: 1.4rem 0; }
-- .pp__meta-wide { grid-column: 1 / -1; }
++ .pp__meta { display: flex; flex-wrap: wrap; gap: 14px 36px; margin: 1.4rem 0; }
++ .pp__meta-wide { flex-basis: 100%; }
.pp__meta strong { display: block; color: var(--accent); font-family: var(--font-mono); font-size: .72rem; text-transform: uppercase; letter-spacing: .14em; margin-bottom: 6px; }
.pp__meta span { color: var(--muted); font-size: .94rem; }
.pp__tech { display: flex; flex-wrap: wrap; gap: 6px; }
@@@ -105,5 +105,6 @@ const catLabel: Record<string, string> = {
.pp__nav-link .mono { font-size: .72rem; color: var(--dim); letter-spacing: .1em; text-transform: uppercase; }
.pp__nav-link:hover { border-color: rgba(var(--accent-rgb), .5); transform: translateY(-3px); text-decoration: none; }
.pp__nav-link--next { text-align: right; }
++ @media (max-width: 560px) { .pp { padding-bottom: 48px; } .pp__prose { padding: 18px; } }
@media (max-width: 820px) { .pp__body { grid-template-columns: 1fr; } .pp__side { position: static; } .pp__nav { grid-template-columns: 1fr; } .pp__nav-link--next { text-align: left; } }
</style>
ddiff --git a/src/pages/resume.astro b/src/pages/resume.astro
iindex 70e17c3..bddbb10 100644
---- a/src/pages/resume.astro
++++ b/src/pages/resume.astro
@@@ -116,6 +116,7 @@ const pitchParts = linkify(profile.pitch, LINK_TERMS);
.resume__contact a:hover { color: var(--accent); text-decoration: none; }
.resume__contact i { color: var(--accent); width: 14px; }
.resume__actions { display: flex; gap: 10px; flex-wrap: wrap; padding: 22px 0 6px; }
++ @media (max-width: 560px) { .resume__actions { display: grid; grid-template-columns: 1fr; } .resume__actions > .btn { justify-content: center; } }
.resume__pdf-note { font-size: .82rem; color: var(--dim); max-width: 62ch; margin: 0 0 8px; }
.resume__pdf-note strong { color: var(--muted); }
.themed-pdf-caret { font-size: .68em; transition: transform .3s var(--ease); }
ddiff --git a/src/styles/global.css b/src/styles/global.css
iindex f29caa1..7f5ac2e 100644
---- a/src/styles/global.css
++++ b/src/styles/global.css
@@@ -55,6 +55,22 @@ body::after {
animation: grain 1.4s steps(4) infinite;
}
:root[data-theme="light"] body::after { opacity: .035; }
++/* Phones: the grid was so faint (and masked so tight) that only the jittering grain showed, which
++ read as the screen glitching. Here the grid is clearly there over more of the screen, and the
++ grain is a slower, lighter texture. */
++@media (max-width: 900px) {
++ body::before {
++ background-image:
++ linear-gradient(to right, rgba(160,180,255,.085) 1px, transparent 1px),
++ linear-gradient(to bottom, rgba(160,180,255,.085) 1px, transparent 1px);
++ background-size: 48px 48px;
++ mask-image: linear-gradient(to bottom, transparent 0, transparent 96px, #000 170px),
++ radial-gradient(ellipse 120% 85% at 50% 0%, #000 0%, transparent 80%);
++ -webkit-mask-image: -webkit-linear-gradient(top, transparent 0, transparent 96px, #000 170px),
++ -webkit-radial-gradient(ellipse 120% 85% at 50% 0%, #000 0%, transparent 80%);
++ }
++ body::after { opacity: .035; animation-duration: 4s; }
++}
@keyframes grain {
0% { transform: translate(0, 0); } 25% { transform: translate(-3%, 2%); }
50% { transform: translate(2%, -3%); } 75% { transform: translate(-1%, -1%); } 100% { transform: translate(0, 0); }
@@@ -95,7 +111,7 @@ p { line-height: 1.7; color: var(--muted); margin: 0 0 1em; }
strong { color: var(--fg); font-weight: 600; }
.container { max-width: var(--container); margin: 0 auto; padding: 0 var(--gutter); }
--.section { padding: clamp(52px, 7.5vw, 116px) 0; position: relative; }
++.section { padding: clamp(36px, 6vw, 88px) 0; position: relative; }
/* A hairline between sections plus a faint alternating band so sections still read as distinct
blocks when someone is flicking through the page fast. */
.section + .section::before, main > .section:not(.hero):first-child::before {
@@@ -142,6 +158,9 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
transition: transform .5s var(--ease), border-color .4s var(--ease), box-shadow .5s var(--ease), background .4s var(--ease);
}
.card:hover { border-color: rgba(var(--accent-rgb), .35); box-shadow: var(--shadow-card), var(--shadow-glow); }
++/* Phones: no backdrop blur on cards (little to see through on a flat background, and every blurred
++ card is another layer the phone must composite; dropped layers painted as blank boxes). */
++@media (max-width: 900px) { .card { -webkit-backdrop-filter: none; backdrop-filter: none; } }
/* Pointer-tracked spotlight (set --mx/--my in JS) */
.spot { --mx: 50%; --my: 50%; }
@@@ -163,7 +182,6 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
font: 600 .98rem/1 var(--font-body); letter-spacing: .005em;
cursor: pointer; text-decoration: none; white-space: nowrap;
transition: transform .35s var(--ease), border-color .3s var(--ease), background .3s var(--ease), color .3s var(--ease), box-shadow .35s var(--ease);
-- will-change: transform;
}
.btn:hover { text-decoration: none; border-color: rgba(var(--accent-rgb), .6); background: rgba(var(--accent-rgb), .1); transform: translateY(-1px); }
.btn i { font-size: .95em; }
@@@ -172,6 +190,7 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
highlight and inner ring keep it from reading as one flat slab of color. */
.btn--primary {
--btn-bg: var(--accent); color: var(--on-accent); border-color: transparent; overflow: hidden;
++ text-shadow: 0 1px 1px rgba(0, 0, 0, .18);
background-image: linear-gradient(180deg, rgba(255,255,255,.22), rgba(255,255,255,0) 60%);
box-shadow: inset 0 1px 0 rgba(255,255,255,.28), inset 0 0 0 1px rgba(0,0,0,.08), 0 12px 30px -12px rgba(var(--accent-rgb), .75);
}
ddiff --git a/src/styles/tokens.css b/src/styles/tokens.css
iindex a966ab8..f5f8607 100644
---- a/src/styles/tokens.css
++++ b/src/styles/tokens.css
@@@ -22,7 +22,7 @@
--accent-3: #3ee6b0;
--accent-3-rgb: 62, 230, 176;
--violet: #8b5cf6;
-- --on-accent: #06091a;
++ --on-accent: #ffffff;
--grad: linear-gradient(95deg, var(--accent) 0%, var(--violet) 48%, var(--accent-2) 100%);
--grad-soft: linear-gradient(135deg, rgba(var(--accent-rgb), .22), rgba(139, 92, 246, .18) 50%, rgba(var(--accent-2-rgb), .16));