openvibers/afresume Commits

Phone fixes: section label, cards that stayed put, company sites as links

OP OpenVibers committed 26 Sept 2026 · f64fe01cef · 5 files, +29 −12

- Cards dropped onto the next one after a hover or tap: the tilt cleared its inline transform on
  pointerleave, and the reveal's end state lived in that same inline style, so the pre-reveal
  translateY(26px) came back. The reveal now adds .is-in instead.
- Blank square tiles in "What I build and do" on phones: the hero entrances held their final frame
  with a forwards fill, keeping every tag and letter on its own GPU layer. They fill backwards now and
  end on their own visible style. Tag hover lift only for real hover pointers; no square tap flash.
- The header's section chip keeps its label on phones (smaller type, ellipsis before it can crowd
  the PDF, palette and menu buttons).
- Experience: a company with a site shows it as a link where the location was (powerchat.live,
  openvibe.live and 20+ domains).

Changes

ddiff --git a/src/components/Hero.astro b/src/components/Hero.astro
iindex 83afd47..2c9b53c 100644
---- a/src/components/Hero.astro
++++ b/src/components/Hero.astro
@@@ -101,7 +101,10 @@ import fcAlbum from "../data/fc-album.json";
.hero__veil { position: absolute; inset: 0; z-index: -1; background: linear-gradient(180deg, rgba(7,9,15,.35) 0%, transparent 30%, transparent 55%, var(--bg) 100%), radial-gradient(60% 50% at 30% 45%, rgba(7,9,15,.35), transparent 70%); }
:root[data-theme="light"] .hero__veil { background: linear-gradient(180deg, transparent 55%, var(--bg) 100%); }
-- .hero__in { opacity: 0; animation: heroIn .9s var(--ease) forwards; animation-delay: var(--d, 0ms); }
++ /* Entrances fill backwards: hidden during their delay, then the element's own (visible) style. A
++ forwards fill kept every animated element on its own GPU layer for good, and phones dropped some
++ of those layers' paint (blank square tiles where the first row of tags should be). */
++ .hero__in { animation: heroIn .9s var(--ease) backwards; animation-delay: var(--d, 0ms); }
.hero__in--scale { animation-name: heroInScale; }
@keyframes heroIn { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }
@keyframes heroInScale { from { opacity: 0; transform: scale(.94); } to { opacity: 1; transform: none; } }
@@@ -126,16 +129,14 @@ import fcAlbum from "../data/fc-album.json";
}
.hero__word { display: inline-flex; white-space: nowrap; }
.hero__ch {
-- display: inline-block; opacity: 0;
-- transform: translateY(.6em) rotate(6deg) scale(.9);
-- filter: blur(6px);
-- animation: chIn .9s var(--ease) forwards;
++ display: inline-block;
++ animation: chIn .9s var(--ease) backwards;
animation-delay: calc(var(--i) * 45ms + 120ms);
transition: color .3s;
}
.hero__ch:hover { color: var(--accent); }
.hero__ch--last::after { content: "."; display: inline-block; color: transparent; background: var(--grad); -webkit-background-clip: text; background-clip: text; opacity: 0; animation: dotIn .8s var(--ease) forwards 1s; }
-- @keyframes chIn { to { opacity: 1; transform: none; filter: blur(0); } }
++ @keyframes chIn { from { opacity: 0; transform: translateY(.6em) rotate(6deg) scale(.9); filter: blur(6px); } to { opacity: 1; transform: none; filter: blur(0); } }
@keyframes dotIn { 0% { opacity: 0; transform: scale(0) translateY(.2em); } 60% { transform: scale(1.3); } 100% { opacity: 1; transform: none; } }
.hero__role {
@@@ -233,10 +234,11 @@ import fcAlbum from "../data/fc-album.json";
display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 12px; min-height: 44px;
background: var(--surface); border: 1px solid var(--border); color: var(--fg);
font: 500 .82rem/1.2 var(--font-body); letter-spacing: -.005em;
-- opacity: 0; animation: tagIn .6s var(--ease) forwards; animation-delay: calc(1000ms + var(--i) * 45ms);
++ animation: tagIn .6s var(--ease) backwards; animation-delay: calc(1000ms + var(--i) * 45ms);
transition: border-color .3s var(--ease), background .3s var(--ease), transform .3s var(--ease);
}
-- .hero__tag:hover { border-color: rgba(var(--accent-rgb), .5); background: var(--surface-2); transform: translateY(-2px); }
++ .hero__tag:hover { border-color: rgba(var(--accent-rgb), .5); background: var(--surface-2); }
++ @media (hover: hover) and (pointer: fine) { .hero__tag:hover { transform: translateY(-2px); } }
@keyframes tagIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.hero__tag-ico { flex: none; width: 28px; height: 28px; border-radius: 9px; display: inline-grid; place-items: center; font-size: .78rem; background: rgba(var(--accent-rgb), .14); color: var(--accent); }
.hero__tag:nth-child(3n+2) .hero__tag-ico { background: rgba(var(--accent-2-rgb), .14); color: var(--accent-2); }
ddiff --git a/src/components/Nav.astro b/src/components/Nav.astro
iindex 25cc6fa..79c7a3e 100644
---- a/src/components/Nav.astro
++++ b/src/components/Nav.astro
@@@ -144,9 +144,14 @@ const path = Astro.url.pathname;
@keyframes whereIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
@media (max-width: 920px) { .nav__where { margin-left: auto; } }
/* Phones: the drawer has its own dark-mode switch, so the standalone toggle goes; the section
-- chip drops its label below 520px so the PDF button, palette and burger always fit. */
++ chip keeps its label (an icon alone reads as a stray button) in smaller type, and shrinks with an
++ ellipsis before it can push the PDF button, palette or burger out. */
@media (max-width: 560px) { .nav__inner { gap: 8px; padding: 0 8px 0 10px; } .nav__icon[data-theme-toggle] { display: none; } }
-- @media (max-width: 520px) { .nav__where .nav__where-txt { display: none; } .nav__where { padding: 0 10px; } }
++ @media (max-width: 520px) {
++ .nav__where { flex: 0 1 auto; min-width: 0; gap: 6px; padding: 0 10px 0 9px; font-size: .64rem; letter-spacing: .05em; }
++ .nav__where i { flex: none; font-size: .72rem; }
++ .nav__where-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
++ }
.nav__links { flex: 1; display: flex; justify-content: center; }
.nav__links ul { position: relative; list-style: none; margin: 0; padding: 4px; display: flex; gap: 2px; border-radius: 999px; }
ddiff --git a/src/components/Timeline.astro b/src/components/Timeline.astro
iindex 6b28030..8ae362f 100644
---- a/src/components/Timeline.astro
++++ b/src/components/Timeline.astro
@@@ -36,7 +36,9 @@ const kindIcon: Record<string, string> = {
<h3 class="tl__role">{e.data.role}</h3>
<div class="tl__co">
{e.data.link ? <a href={e.data.link} target="_blank" rel="noopener">{e.data.company}</a> : <span>{e.data.company}</span>}
-- {e.data.location && <span class="tl__loc"><i class="fa-solid fa-location-dot"></i> {e.data.location}</span>}
++ {e.data.link
++ ? <a class="tl__loc tl__site" href={e.data.link} target="_blank" rel="noopener"><i class="fa-solid fa-globe" aria-hidden="true"></i> {e.data.location?.includes(new URL(e.data.link).hostname) ? e.data.location : new URL(e.data.link).hostname}<i class="fa-solid fa-arrow-up-right-from-square tl__site-go" aria-hidden="true"></i></a>
++ : e.data.location && <span class="tl__loc"><i class="fa-solid fa-location-dot"></i> {e.data.location}</span>}
</div>
</header>
{e.data.summary && <p class="tl__sum">{e.data.summary}</p>}
@@@ -75,6 +77,9 @@ const kindIcon: Record<string, string> = {
.tl__co { display: flex; flex-wrap: wrap; gap: 6px 18px; color: var(--muted); font-size: .95rem; }
.tl__co a { color: var(--fg); font-weight: 600; }
.tl__loc { color: var(--dim); font-size: .85rem; display: inline-flex; align-items: center; gap: 6px; }
++ .tl__co a.tl__site { color: var(--accent); font-weight: 500; text-decoration: none; }
++ .tl__co a.tl__site:hover { text-decoration: underline; text-underline-offset: 3px; }
++ .tl__site-go { font-size: .7em; opacity: .75; }
.tl__sum { font-size: .95rem; color: var(--muted); font-style: italic; border-left: 2px solid rgba(var(--accent-rgb), .4); padding-left: 12px; }
.tl__bullets { margin: 0; padding: 0 0 0 1.1rem; display: grid; gap: 8px; color: var(--muted); line-height: 1.65; font-size: .95rem; }
.tl__bullets li::marker { color: var(--accent-2); }
ddiff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
iindex dd7230c..637087b 100644
---- a/src/layouts/Base.astro
++++ b/src/layouts/Base.astro
@@@ -254,7 +254,7 @@ const imageUrl = new URL(image, site).toString();
: kind === 'right' ? { opacity: 0, transform: 'translateX(12px)' }
: kind === 'scale' ? { opacity: 0, transform: 'scale(.975)' }
: { opacity: 0, transform: 'translateY(8px) scale(.99)' };
-- el.style.opacity = '1'; el.style.transform = 'none';
++ el.classList.add('is-in');
el.animate([from, { opacity: 1, transform: 'none' }], { duration: 600, delay, easing: 'cubic-bezier(0.22, 1, 0.36, 1)', fill: 'backwards' });
io.unobserve(el);
}
ddiff --git a/src/styles/global.css b/src/styles/global.css
iindex f11bd31..f29caa1 100644
---- a/src/styles/global.css
++++ b/src/styles/global.css
@@@ -14,6 +14,8 @@ html {
font-family: var(--font-body);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
++ /* Cards and buttons give their own press feedback; the browser's square tap flash drew over rounded ones. */
++ -webkit-tap-highlight-color: transparent;
scroll-padding-top: calc(var(--nav-h) + 12px);
}
html:not(.lenis) { scroll-behavior: smooth; }
@@@ -211,6 +213,9 @@ main > .section:nth-of-type(even) { background: linear-gradient(180deg, transpar
[data-reveal="right"] { transform: translateX(30px); }
[data-reveal="scale"] { transform: scale(.94); }
.no-js [data-reveal] { opacity: 1; transform: none; }
++/* Revealed: a class, not inline styles, so a card's tilt can clear its own inline transform on
++ pointerleave without the pre-reveal offset above coming back (it dropped cards onto the next one). */
++[data-reveal].is-in { opacity: 1; transform: none; }
/* ---- Utilities ------------------------------------------------------------ */
.grad-text { background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; }