Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 404.html
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ <h1 id="missing-title" class="gsap-title">Nothing here<span class="signal">.</sp
</main>
</div>

<script defer src="https://cdn.jsdelivr.net/npm/barba.js@2.9.7/dist/barba.umd.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/@barba/core@2.9.7/dist/barba.umd.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/SplitText.min.js"></script>
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ Each project route is directly refreshable, uses root-safe asset paths, provides

`barba-layer.js` owns route replacement, namespace-aware transitions, metadata updates, announcements, focus handoff, hash scrolling, and scroll restoration. `site-core.js` owns the singleton global shell, one optional Lenis instance, page reveal setup, and explicit cleanup registration.

GSAP owns SplitText, ScrollTrigger, and restrained project-index movement. Anime.js owns page-scoped SVG rule drawing, evidence counts, and the homepage intro dash. Motion owns page-scoped hover springs and the homepage-only scroll rule. Every page module returns cleanup handles; generated SVG nodes, observers, timers, split instances, hover subscriptions, tweens, and ScrollTriggers are released before the next container is initialized.
GSAP owns glyph-safe SplitText line reveals, ScrollTrigger, and restrained project-index movement. Barba owns the direction-aware signal-red paper wipe and temporary title/index handoff clones; each clone exists only while its source route is present and is removed on normal completion, cleanup, or fail-open release. Anime.js owns page-scoped SVG rule drawing, evidence counts, and the homepage intro dash. Motion owns page-scoped hover springs and the homepage-only scroll rule. Every page module returns cleanup handles; generated SVG nodes, observers, timers, split instances, hover subscriptions, handoff clones, tweens, and ScrollTriggers are released before the next container is initialized.

The transition overlay has a fail-open timeout. If Barba, a CDN module, or a transition callback fails, native browser navigation remains available and content is not hidden behind JavaScript. Content is visible by default; `.js` only opts into enhancement styles. A document-level watchdog and page-level watchdog reveal content after 1.5 seconds. `prefers-reduced-motion: reduce` disables smooth scrolling, transition travel, counters, parallax, and reveal hiding.
The transition overlay has a fail-open timeout. If Barba, a CDN module, or a transition callback fails, native browser navigation remains available and content is not hidden behind JavaScript. Content is visible by default; `.js` only opts into enhancement styles. A document-level watchdog and page-level watchdog reveal content after 1.5 seconds. `prefers-reduced-motion: reduce` disables smooth scrolling, transition travel, counters, parallax, title travel, and reveal hiding. Title line wrappers remain overflow-visible with padded line boxes; no SplitText mask is used for glyph cropping.

## Content truth

Project copy is limited to evidence present in the verified portfolio record and source repositories. Klar claims are grounded in its React/Vite client, Express/MongoDB server, Adzuna/JSearch ingestion, rate-aware requests, day-bucketed skill history, and analytics routes. Pakka claims are grounded in its Next.js/React/Supabase/PostgreSQL stack, authenticated transition RPCs, explicit authorization and state checks, ordered row locking, numeric monetary fields, partial unique indexes, operation-specific duplicate protection, and paired transfer paths. The detailed Pakka case study labels its approved aggregate counts as “Deployed schema audit snapshot — 27 August 2026”: 4 transition RPCs, 38 deployed migration records, and 45 deployed public RLS policies. It makes no product-traction or verified lifecycle-instrumentation claim and uses a concise measurement boundary instead. Hyderabad Metro Go claims are grounded in the actual vanilla JavaScript route engine, deque-based 0-1 BFS, three line maps, 59 stations, three interchange nodes, ten fare slabs, facilities, parking, and five-item recent-trip localStorage. Bhasha Seva uses only resume/certificate-backed facts and has no deployed launch link.
Project copy is limited to evidence present in the verified portfolio record and source repositories. Klar claims are grounded in its React/Vite client, Express/MongoDB server, Adzuna/JSearch ingestion, rate-aware requests, normalized shared job shape, count-time company/title/city dedupe, curated 46-entry taxonomy, day-bucketed skill history, bounded in-process aggregate caches, and timestamped public dataset status. Its detailed case study may show only the dated public API snapshot of 270 deduplicated 12-month postings and 43 observed skills versus 46 canonical taxonomy entries. It does not publish Mongo deployment counts, cluster schema parity, salary-disclosure counters, Watchlist/user data, or private analytics. Pakka claims are grounded in its Next.js/React/Supabase/PostgreSQL stack, authenticated transition RPCs, explicit authorization and state checks, ordered row locking, numeric monetary fields, partial unique indexes, operation-specific duplicate protection, and paired transfer paths. The detailed Pakka case study labels its approved aggregate counts as “Deployed schema audit snapshot — 27 August 2026”: 4 transition RPCs, 38 deployed migration records, and 45 deployed public RLS policies. It makes no product-traction or verified lifecycle-instrumentation claim and uses a concise measurement boundary instead. Hyderabad Metro Go claims are grounded in the actual vanilla JavaScript route engine, deque-based 0-1 BFS, three line maps, 59 stations, three interchange nodes, ten fare slabs, facilities, parking, and five-item recent-trip localStorage. Bhasha Seva uses only resume/certificate-backed facts and has no deployed launch link.

## Local preview

Expand Down
6 changes: 5 additions & 1 deletion anime-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ import { animate, createTimeline, onScroll, stagger, svg } from 'https://cdn.jsd
}));
});

if (namespace === 'home') {
if (namespace === 'home' && portfolio.page.skipHomeIntro) {
container.querySelector('.intro-curtain')?.remove();
}

if (namespace === 'home' && !portfolio.page.skipHomeIntro) {
const intro = createTimeline({ autoplay: false });
intro
.add(container.querySelector('.intro-dash'), { scaleX: [0, 1], duration: 650, ease: 'outExpo' })
Expand Down
40 changes: 23 additions & 17 deletions gsap-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,33 @@
const makeTitleTween = (element) => {
const isHero = element.id === 'case-title' || element.id === 'hero-title';
const split = SplitText.create(element, {
type: 'words, lines',
mask: 'lines',
type: 'lines',
linesClass: 'line',
autoSplit: true,
aria: 'auto',
onSplit(self) {
const tween = gsap.from(self.lines, {
yPercent: 110,
opacity: 1,
duration: 1,
stagger: 0.08,
ease: 'power3.out',
delay: isHero ? 0.18 : 0,
scrollTrigger: isHero ? undefined : {
trigger: element,
start: 'top 80%',
once: true
},
onComplete: () => {
if (!disposed) gsap.delayedCall(0.05, () => self.revert?.());
if (disposed) return;
const lines = Array.from(self.lines || []);
if (!lines.length) return;
const tween = gsap.fromTo(lines,
{ yPercent: 72, opacity: 0 },
{
yPercent: 0,
opacity: 1,
duration: isHero ? 0.92 : 0.78,
stagger: isHero ? 0.095 : 0.07,
delay: isHero ? 0.16 : 0,
ease: 'power3.out',
scrollTrigger: isHero ? undefined : {
trigger: element,
start: 'top 82%',
once: true
},
onComplete: () => {
if (!disposed) gsap.set(lines, { clearProps: 'transform,opacity' });
}
}
});
);
tweens.push(tween);
return tween;
}
Expand Down
20 changes: 13 additions & 7 deletions motion-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,30 @@ import { animate, hover, scroll } from 'https://cdn.jsdelivr.net/npm/motion@13.1
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
const signal = '#ee4c35';

const bindHover = (elements) => {
const bindHover = (elements, options = {}) => {
const cancels = [];
const amplitudeX = options.amplitudeX ?? 4;
const amplitudeY = options.amplitudeY ?? 3;
const tintEnabled = options.tint !== false;
elements.forEach((element) => {
const cancel = hover(element, (target, startEvent) => {
const rect = target.getBoundingClientRect();
const pointerX = rect.width ? (startEvent.clientX - rect.left) / rect.width - 0.5 : 0;
const pointerY = rect.height ? (startEvent.clientY - rect.top) / rect.height - 0.5 : 0;
const baseColor = getComputedStyle(target).color;
const shift = animate(target, { x: pointerX * 4, y: pointerY * 3 }, {
const shift = animate(target, { x: pointerX * amplitudeX, y: pointerY * amplitudeY }, {
type: 'spring', stiffness: 520, damping: 28, mass: 0.5
});
const tint = animate(target, { color: signal }, { duration: 0.18, ease: 'easeOut' });
const tint = tintEnabled
? animate(target, { color: signal }, { duration: 0.18, ease: 'easeOut' })
: { stop() {} };
return () => {
shift.stop();
tint.stop();
animate(target, { x: 0, y: 0 }, {
type: 'spring', stiffness: 520, damping: 32, mass: 0.5
});
animate(target, { color: baseColor }, { duration: 0.16, ease: 'easeOut' });
if (tintEnabled) animate(target, { color: baseColor }, { duration: 0.16, ease: 'easeOut' });
};
});
cancels.push(cancel);
Expand All @@ -36,11 +41,12 @@ import { animate, hover, scroll } from 'https://cdn.jsdelivr.net/npm/motion@13.1
const module = {
initGlobal() {
if (reduceMotion.matches) return () => {};
return bindHover(document.querySelectorAll('.nav-links a, .brand'));
return bindHover(document.querySelectorAll('.nav-links a, .brand'), { amplitudeX: 2.4, amplitudeY: 1.4 });
},
initPage(container) {
if (reduceMotion.matches) return () => {};
const cleanupHover = bindHover(container.querySelectorAll('.project-link, .case-link, .case-nav a, .contact a'));
const cleanupHover = bindHover(container.querySelectorAll('.project-link, .case-link, .case-nav a, .contact a'), { amplitudeX: 4, amplitudeY: 2.4 });
const cleanupRows = bindHover(container.querySelectorAll('.project-entry'), { amplitudeX: 3.5, amplitudeY: 1.4, tint: false });
const ruleBar = container.querySelector('.scroll-rule span');
let cleanupScroll = () => {};
if (ruleBar) {
Expand All @@ -49,7 +55,7 @@ import { animate, hover, scroll } from 'https://cdn.jsdelivr.net/npm/motion@13.1
}, { ease: 'linear' });
cleanupScroll = scroll(progressAnimation);
}
return () => { cleanupHover(); cleanupScroll(); };
return () => { cleanupHover(); cleanupRows(); cleanupScroll(); };
}
};

Expand Down
7 changes: 7 additions & 0 deletions site-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@
if (root.global.initialized) return;
root.global.initialized = true;

if (!document.querySelector('link[href="/visual-fixes.css"]')) {
const fixSheet = document.createElement('link');
fixSheet.rel = 'stylesheet';
fixSheet.href = '/visual-fixes.css';
document.head.appendChild(fixSheet);
Comment on lines +73 to +77

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Runtime-only visual stylesheet

Loading visual-fixes.css from initGlobal() excludes the new alignment and glyph-spacing rules from initial static rendering and the documented no-JavaScript fallback, while asynchronous loading can also restyle titles after first paint. Link this stylesheet directly from the route documents so these presentation rules are available before page initialization.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

}

const toggle = document.querySelector('.menu-toggle');
const navLinks = document.querySelector('.nav-links');
const closeMenu = () => {
Expand Down
64 changes: 64 additions & 0 deletions visual-fixes.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* Visual QA overrides for Klar actions/facts and Bodoni glyph crop. */
.case-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.85rem 1.6rem;
}
.case-actions .project-link,
.case-actions .text-link {
margin-top: 0;
line-height: 1;
}
.text-link {
display: inline-flex;
align-items: center;
color: var(--ink);
font: 700 .7rem/1 var(--sans);
letter-spacing: .12em;
text-decoration: none;
text-transform: uppercase;
padding-bottom: .12rem;
border-bottom: 1px solid var(--ink);
transition: color .2s ease, border-color .2s ease, transform .3s var(--ease);
}
.text-link span {
display: inline-block;
margin-left: .35rem;
color: var(--signal);
transition: transform .35s var(--ease);
}
.text-link:hover {
color: var(--signal);
border-color: var(--signal);
}
.text-link:hover span {
transform: translateX(.35rem);
}
.case-facts > div {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);
column-gap: 1.25rem;
align-items: baseline;
}
.case-facts dt {
min-width: 0;
}
.case-facts dd {
margin: 0;
justify-self: end;
text-align: right;
}
.gsap-title .line {
display: block;
overflow: visible;
padding: 0.16em 0.1em 0.28em;
margin: -0.08em -0.06em -0.16em;
will-change: transform, opacity;
}
.case-hero h1 {
line-height: 0.94;
letter-spacing: -0.06em;
overflow: visible;
padding-block: 0.1em 0.2em;
}