patx/afterdarklabs
indexerd.html 2
Commit 9bd06c7 · patx · 2026-09-03T01:13:04-04:00
Comments
No comments yet.
Diff
diff --git a/docs/indexerd.html b/docs/indexerd.html
index 123ee56..b9a801d 100644
--- a/docs/indexerd.html
+++ b/docs/indexerd.html
@@ -74,8 +74,7 @@
font-size: 125%;
}
- nav img[src*="/logo_assets/mark"],
- footer img[src*="/logo_assets/mark"] {
+ nav img[src*="/logo_assets/mark"] {
width: 3rem;
height: 3rem;
}
@@ -202,30 +201,62 @@
}
.nav-brand-ready .nav-brand {
- visibility: hidden;
- opacity: 0;
- clip-path: inset(20% 100% 20% 0 round 1rem);
- filter: blur(0.3rem);
+ visibility: visible;
+ opacity: 1;
pointer-events: none;
- transform: translate3d(-1.5rem, 0, 0) skewX(-10deg) scaleX(0.45);
- transition:
- opacity 160ms ease,
- transform 280ms ease,
- filter 220ms ease,
- clip-path 280ms ease,
- visibility 0s linear 280ms;
- will-change: transform, opacity, clip-path, filter;
+ transform: none;
}
.nav-brand-ready .nav-brand.is-visible {
- visibility: visible;
- opacity: 1;
- clip-path: none;
- filter: none;
pointer-events: auto;
- transform: none;
- animation: nav-brand-swish-in 720ms cubic-bezier(0.16, 1, 0.3, 1);
- transition: none;
+ }
+
+ .nav-cta {
+ display: inline-flex;
+ min-height: 3.25rem;
+ align-items: center;
+ justify-content: center;
+ padding: 0.75rem 1.15rem;
+ font-size: clamp(1.05rem, 5vw, 1.3rem);
+ line-height: 1;
+ letter-spacing: -0.035em;
+ }
+
+ .nav-brand-word {
+ display: flex;
+ padding-right: 0.08em;
+ font-size: clamp(2.3rem, 11vw, 2.8rem);
+ line-height: 0.76;
+ letter-spacing: -0.08em;
+ }
+
+ .nav-brand-letter {
+ display: inline-block;
+ opacity: var(--nav-letter-opacity, 1);
+ filter: blur(var(--nav-letter-blur, 0));
+ transform:
+ translate3d(var(--nav-letter-x, 0), var(--nav-letter-y, 0), 0)
+ rotate(var(--nav-letter-rotate, 0))
+ skewX(var(--nav-letter-skew, 0))
+ scale(var(--nav-letter-scale-x, 1), var(--nav-letter-scale-y, 1));
+ transform-origin: center;
+ will-change: opacity, filter, transform;
+ }
+
+ .nav-brand-ready .nav-brand-letter {
+ opacity: var(--nav-letter-opacity, 0);
+ }
+
+ @media (min-width: 1024px) {
+ .nav-brand-word {
+ font-size: clamp(2.3rem, 4vw, 3.3rem);
+ }
+
+ .nav-cta {
+ min-height: 2.75rem;
+ padding: 0.72rem 1.35rem;
+ font-size: clamp(1.2rem, 2vw, 1.6rem);
+ }
}
.hero-vignette {
@@ -269,33 +300,13 @@
letter-spacing: -0.075em;
}
- @keyframes nav-brand-swish-in {
- 0% {
- opacity: 0;
- clip-path: polygon(0 43%, 0 43%, 0 57%, 0 57%);
- filter: blur(0.45rem);
- transform: translate3d(-3rem, 0.25rem, 0) skewX(-22deg) scale(0.3, 1.28);
- }
- 38% {
- opacity: 1;
- clip-path: polygon(0 -7%, 104% 6%, 97% 108%, -2% 91%);
- filter: blur(0);
- transform: translate3d(0.7rem, -0.08rem, 0) skewX(8deg) scale(1.08, 0.88);
- }
- 58% {
- clip-path: polygon(0 5%, 98% -4%, 103% 95%, 2% 103%);
- transform: translate3d(-0.3rem, 0.04rem, 0) skewX(-5deg) scale(0.96, 1.08);
- }
- 78% {
- clip-path: polygon(0 -1%, 101% 2%, 99% 101%, 0 98%);
- transform: translate3d(0.12rem, 0, 0) skewX(2deg) scale(1.02, 0.98);
- }
- 100% {
- opacity: 1;
- clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
- filter: blur(0);
- transform: none;
- }
+ .hero-letter {
+ display: inline-block;
+ opacity: var(--hero-letter-opacity, 1);
+ filter: blur(var(--hero-letter-blur, 0));
+ transform: scale(var(--hero-letter-scale, 1));
+ transform-origin: center;
+ will-change: opacity, filter, transform;
}
@keyframes neon-letter-flicker {
@@ -331,11 +342,14 @@
.work-intro-stage {
position: sticky;
top: var(--site-nav-height);
+ z-index: 3;
display: flex;
box-sizing: border-box;
align-items: flex-start;
overflow: hidden;
padding: clamp(0.75rem, 1.5vw, 1.25rem) clamp(1.5rem, 5vw, 4rem);
+ transform: translate3d(0, var(--work-intro-shift, 0), 0);
+ will-change: transform;
}
.work-heading {
@@ -361,16 +375,48 @@
display: inline-block;
}
+ .work-heading-email {
+ color: inherit;
+ font-weight: 500;
+ text-decoration: none;
+ transition: font-weight 160ms ease;
+ }
+
+ .work-heading-email:hover,
+ .work-heading-email:focus-visible {
+ font-weight: 700;
+ }
+
+ .work-heading-email:focus-visible {
+ outline: 0.055em solid #000;
+ outline-offset: 0.075em;
+ }
+
+ .work-intro--closing {
+ height: auto;
+ min-height: 0;
+ }
+
+ .work-intro--closing .work-intro-stage {
+ position: relative;
+ top: auto;
+ transform: none;
+ will-change: auto;
+ }
+
.work-intro.is-scroll-ready .work-heading-word {
opacity: 0;
filter: blur(0.18em);
transform: translate3d(0, 0.42em, 0) rotate(1.5deg);
transform-origin: left bottom;
+ will-change: opacity, filter, transform;
+ }
+
+ .work-intro.is-scroll-ready.is-motion-ready .work-heading-word {
transition:
opacity 220ms ease,
filter 360ms ease,
transform 420ms cubic-bezier(0.16, 1, 0.3, 1);
- will-change: opacity, filter, transform;
}
.work-intro.is-scroll-ready .work-heading-word.is-revealed {
@@ -380,10 +426,10 @@
}
@media (prefers-reduced-motion: reduce) {
- .nav-brand-ready .nav-brand,
- .nav-brand-ready .nav-brand.is-visible {
- animation: none;
- transition: none;
+ .nav-brand-letter {
+ filter: none;
+ transform: none;
+ will-change: auto;
}
.neon-letter,
@@ -391,6 +437,13 @@
animation: none;
}
+ .hero-letter {
+ opacity: 1;
+ filter: none;
+ transform: none;
+ will-change: auto;
+ }
+
.work-intro {
height: auto;
}
@@ -398,6 +451,8 @@
.work-intro-stage {
position: relative;
top: auto;
+ transform: none;
+ will-change: auto;
}
.work-intro.is-scroll-ready .work-heading-word {
@@ -410,7 +465,7 @@
.work-stage {
box-sizing: border-box;
- padding: clamp(0.75rem, 1.5vw, 1.5rem) clamp(1rem, 2vw, 2rem) clamp(5rem, 9vw, 8rem);
+ padding: clamp(0.75rem, 1.5vw, 1.5rem) clamp(1rem, 2vw, 2rem);
}
.work-stage-content {
@@ -437,14 +492,14 @@
border-radius: 0;
}
- #work-scroller > article > a {
+ #work-scroller > article > .portfolio-media {
display: block;
width: 100%;
height: 100%;
border-radius: 0;
}
- #work-scroller > article > a > img {
+ #work-scroller > article > .portfolio-media > img {
width: 100%;
height: 100%;
aspect-ratio: auto;
@@ -452,7 +507,7 @@
transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
- #work-scroller > article > div {
+ #work-scroller > article > .portfolio-details {
position: absolute;
inset: 0;
z-index: 2;
@@ -474,17 +529,16 @@
backdrop-filter: blur(0.35rem);
}
- #work-scroller > article:hover > div,
- #work-scroller > article:focus-within > div,
- #work-scroller > article.is-description-visible > div {
+ #work-scroller > article:focus-within > .portfolio-details,
+ #work-scroller > article.is-description-visible > .portfolio-details {
opacity: 1;
clip-path: inset(0);
+ pointer-events: auto;
transform: none;
}
- #work-scroller > article:hover > a > img,
- #work-scroller > article:focus-within > a > img,
- #work-scroller > article.is-description-visible > a > img {
+ #work-scroller > article:focus-within > .portfolio-media > img,
+ #work-scroller > article.is-description-visible > .portfolio-media > img {
transform: scale(1.025);
}
@@ -493,7 +547,7 @@
outline-offset: -2px;
}
- #work-scroller > article > div > p {
+ #work-scroller > article > .portfolio-details > p {
width: 100%;
margin: 0;
color: #000;
@@ -513,6 +567,25 @@
font-weight: 700;
}
+ .portfolio-live-link {
+ display: inline-block;
+ color: #000;
+ font-weight: 500;
+ text-decoration: none;
+ transition: font-weight 160ms ease;
+ white-space: nowrap;
+ }
+
+ .portfolio-live-link:hover,
+ .portfolio-live-link:focus-visible {
+ font-weight: 700;
+ }
+
+ .portfolio-live-link:focus-visible {
+ outline: 0.09em solid #000;
+ outline-offset: 0.08em;
+ }
+
.portfolio-scroll-ready #work-scroller > article {
opacity: var(--card-opacity, 0);
filter: blur(var(--card-blur, 0.4rem));
@@ -532,6 +605,29 @@
}
+ @media (min-width: 768px) and (hover: hover) {
+ #work-scroller > article:hover > .portfolio-details {
+ opacity: 1;
+ clip-path: inset(0);
+ pointer-events: auto;
+ transform: none;
+ }
+
+ #work-scroller > article:hover > .portfolio-media > img {
+ transform: scale(1.025);
+ }
+ }
+
+ @media (max-width: 767px) {
+ #work-scroller > article {
+ cursor: pointer;
+ }
+
+ #work-scroller > article.is-description-visible {
+ cursor: default;
+ }
+ }
+
@media (prefers-reduced-motion: reduce) {
.portfolio-scroll-ready #work-scroller > article {
opacity: 1;
@@ -539,8 +635,8 @@
transform: none;
}
- #work-scroller > article > a > img,
- #work-scroller > article > div {
+ #work-scroller > article > .portfolio-media > img,
+ #work-scroller > article > .portfolio-details {
transition: none;
}
}
@@ -610,18 +706,18 @@
<div class="mx-auto max-w-7xl px-4 py-4 sm:px-6 lg:py-5">
<div class="flex items-center justify-between gap-3 lg:hidden">
<a href="#hero" class="nav-brand flex min-w-0 items-center rounded-2xl p-1.5 pr-3 text-left text-white transition-colors hover:bg-white/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00]" aria-label="Go to top">
- <span class="logo-font block text-2xl font-semibold tracking-tighter">ERD</span>
+ <span class="nav-brand-word logo-font font-semibold" aria-hidden="true"><span class="nav-brand-letter">E</span><span class="nav-brand-letter">R</span><span class="nav-brand-letter">D</span></span>
</a>
- <a href="mailto:[email protected]" class="neon-ink shrink-0 rounded-2xl bg-[#fff] px-4 py-3 text-sm font-semibold transition-colors hover:bg-white">Let's Talk</a>
+ <a href="mailto:[email protected]" class="nav-cta neon-ink shrink-0 rounded-2xl bg-[#fff] font-semibold transition-colors hover:bg-white">Let's Talk</a>
</div>
<div class="hidden items-center justify-between lg:flex">
<a href="#hero" class="nav-brand flex items-center gap-3" aria-label="Go to top">
<div>
- <span class="logo-font block text-2xl font-semibold tracking-tighter">ERD</span>
+ <span class="nav-brand-word logo-font font-semibold" aria-hidden="true"><span class="nav-brand-letter">E</span><span class="nav-brand-letter">R</span><span class="nav-brand-letter">D</span></span>
</div>
</a>
- <a href="mailto:[email protected]" class="neon-ink rounded-2xl bg-[#fff] px-6 py-3 font-semibold transition-colors hover:bg-white">Let's Talk</a>
+ <a href="mailto:[email protected]" class="nav-cta neon-ink rounded-2xl bg-[#fff] font-semibold transition-colors hover:bg-white">Let's Talk</a>
</div>
</div>
</nav>
@@ -632,9 +728,7 @@
<div class="hero-content mx-auto grid w-full max-w-7xl justify-items-center gap-10 px-6 py-32 text-center">
<div>
- <h1 class="heading text-7xl font-bold leading-none tracking-tighter md:text-8xl lg:text-7xl xl:text-8xl">
- ERD
- </h1>
+ <h1 class="heading text-7xl font-bold leading-none tracking-tighter md:text-8xl lg:text-7xl xl:text-8xl" aria-label="ERD"><span class="hero-letter" aria-hidden="true">E</span><span class="hero-letter" aria-hidden="true">R</span><span class="hero-letter" aria-hidden="true">D</span></h1>
</div>
</div>
@@ -668,110 +762,117 @@
<div class="work-stage-content">
<div id="work-scroller" class="portfolio-grid">
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://gardenfreshwholesale.com" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Garden Fresh Wholesale website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/gardenfresh.webp?v=2" width="1200" height="702" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Garden Fresh Wholesale ecommerce web app project by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description">We built <span class="portfolio-name">Garden Fresh Wholesale</span>'s business website and local SEO strategy that increased customer acquisition by 48% since launch.</span>
+ <span class="portfolio-description">We built <span class="portfolio-name">Garden Fresh Wholesale</span>'s business website and local SEO strategy that increased customer acquisition by 48% since launch. <a class="portfolio-live-link" href="https://gardenfreshwholesale.com" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://miadruck.com" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Mia Druck website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/miadruck.webp?v=3" width="1200" height="681" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Mia Druck real estate website project by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description"><span class="portfolio-name">Mia Druck</span> needed a real estate website with heavy local SEO and a custom API for the most up-to-date listings and sales. We delivered this custom site in under a week.</span>
+ <span class="portfolio-description"><span class="portfolio-name">Mia Druck</span> needed a real estate website with heavy local SEO and a custom API for the most up-to-date listings and sales. We delivered this custom site in under a week. <a class="portfolio-live-link" href="https://miadruck.com" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://coralvibefl.com" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Coral Vibe website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/coralvibe.webp?v=1" width="1200" height="678" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Shopify store for Coral Vibe Florida project by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description"><span class="portfolio-name">Coral Vibe</span> hired us to design their Shopify store and provide support for ecommerce operations, product management, and social media marketing. Since launch, store visits resulting in sales have increased by 86%.</span>
+ <span class="portfolio-description"><span class="portfolio-name">Coral Vibe</span> hired us to design their Shopify store and provide support for ecommerce operations, product management, and social media marketing. Since launch, store visits resulting in sales have increased by 86%. <a class="portfolio-live-link" href="https://coralvibefl.com" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://frontlineboats.com" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Frontline Boat Services website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/frontlineboats.webp?v=1" width="1200" height="700" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Frontline Boat Services marine services website project by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description">For <span class="portfolio-name">Frontline Boat Services</span>, we built a highly focused business website with local SEO, custom payment links for customers, and an owner-managed photo gallery.</span>
+ <span class="portfolio-description">For <span class="portfolio-name">Frontline Boat Services</span>, we built a highly focused business website with local SEO, custom payment links for customers, and an owner-managed photo gallery. <a class="portfolio-live-link" href="https://frontlineboats.com" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://voldservices.com" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Vold Services website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/voldservices.webp" width="1200" height="702" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Vold Services exterior cleaning website with a scroll-animated hero, designed by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description"><span class="portfolio-name">Vold Services</span> hired us to create their business website with a scroll-animated hero, local SEO/GEO and an owner-managed photo gallery.</span>
+ <span class="portfolio-description"><span class="portfolio-name">Vold Services</span> hired us to create their business website with a scroll-animated hero, local SEO/GEO and an owner-managed photo gallery. <a class="portfolio-live-link" href="https://voldservices.com" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://vegy.app/" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Vegy website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/vegy.webp?v=1" width="1200" height="702" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Vegy wholesale ordering platform project by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description">We have partnered with <span class="portfolio-name">Vegy</span> since 2023 to create a white-label web app for wholesale ordering with AI and email integrations, real-time updates, and advanced analytics. The app processed over $2 million in orders in the first year.</span>
+ <span class="portfolio-description">We have partnered with <span class="portfolio-name">Vegy</span> since 2023 to create a white-label web app for wholesale ordering with AI and email integrations, real-time updates, and advanced analytics. The app processed over $2 million in orders in the first year. <a class="portfolio-live-link" href="https://vegy.app/" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://prima.vegy.app/" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Prima Produce website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/prima.webp?v=1" width="1200" height="702" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Prima Produce business website project by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description">Our partner <span class="portfolio-name">Prima Produce</span> hired us to build their business website with custom online ordering and cohesive branding. Working with their existing logo, we created a complete online experience for Prima's customers.</span>
+ <span class="portfolio-description">Our partner <span class="portfolio-name">Prima Produce</span> hired us to build their business website with custom online ordering and cohesive branding. Working with their existing logo, we created a complete online experience for Prima's customers. <a class="portfolio-live-link" href="https://prima.vegy.app/" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
<article class="w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl bg-zinc-950 sm:w-[28rem] md:w-[38rem]">
- <a href="https://invoiceotter.com" target="_blank" rel="noopener noreferrer" class="block w-full overflow-hidden rounded-t-3xl text-left focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ccff00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#17212b]" aria-label="Visit Invoice Otter website">
+ <div class="portfolio-media">
<img src="https://afterdarklabs.io/invoiceotter.webp?v=1" width="1200" height="673" loading="lazy" decoding="async" class="aspect-video w-full object-cover" alt="Invoice Otter SaaS project by After Dark Labs">
- </a>
- <div class="p-6">
+ </div>
+ <div class="portfolio-details p-6">
<p class="text-sm font-medium text-white">
- <span class="portfolio-description">We built the mobile app, web interface, and the APIs for <span class="portfolio-name">Invoice Otter</span>'s contractor invoicing and estimating solutions with subscriptions, integrated payments, a custom AI chatbot, and per-user customization.</span>
+ <span class="portfolio-description">We built the mobile app, web interface, and the APIs for <span class="portfolio-name">Invoice Otter</span>'s contractor invoicing and estimating solutions with subscriptions, integrated payments, a custom AI chatbot, and per-user customization. <a class="portfolio-live-link" href="https://invoiceotter.com" target="_blank" rel="noopener noreferrer">See live site</a></span>
</p>
</div>
</article>
</div>
</div>
</div>
+
+ <div class="work-intro work-intro--closing" aria-labelledby="closing-heading">
+ <div class="work-intro-stage">
+ <h2 id="closing-heading" class="work-heading">
+ <span class="work-heading-words">
+ <span class="work-heading-word">Have</span>
+ <span class="work-heading-word">a</span>
+ <span class="work-heading-word">big</span>
+ <span class="work-heading-word">idea?</span>
+ <span class="work-heading-word">Let’s</span>
+ <span class="work-heading-word">make</span>
+ <span class="work-heading-word">it</span>
+ <span class="work-heading-word">impossible</span>
+ <span class="work-heading-word">to</span>
+ <span class="work-heading-word">ignore.</span>
+ <span class="work-heading-word">Tell</span>
+ <span class="work-heading-word">us</span>
+ <span class="work-heading-word">what’s</span>
+ <span class="work-heading-word">next</span>
+ <span class="work-heading-word">→</span>
+ <a class="work-heading-word work-heading-email" href="mailto:[email protected]">[email protected]</a>
+ </span>
+ </h2>
+ </div>
+ </div>
</section>
</main>
- <footer class="border-t border-white/10 bg-black py-16">
- <div class="mx-auto max-w-7xl px-6 text-center">
- <a href="#hero" class="mb-6 inline-flex items-center justify-center gap-3">
- <span class="min-w-0 text-left">
- <span class="logo-font block text-3xl font-semibold tracking-tighter">ERD</span>
- </span>
- </a>
- <p style="color: #000;">
- <a href="https://share.google/inOLBEb4neFhKNkWb" target="_blank" rel="noopener noreferrer" class="transition-colors hover:text-[#ccff00]">★★★★★ on Google</a>
- <br>
- Let's work together → <a href="mailto:[email protected]" class="transition-colors hover:text-[#ccff00]" style="margin-top:50px;">[email protected]</a>
- <br><br>
- © 2026 <a href="https://harrisonerd.com/" target="_blank" rel="noopener noreferrer" class="transition-colors hover:text-[#ccff00]">Harrison Erd</a>
- </p>
-
- </div>
- </footer>
-
<script>
(function () {
const hero = document.getElementById('hero');
@@ -817,57 +918,158 @@
(function () {
const hero = document.getElementById('hero');
+ const heading = hero?.querySelector('.hero-content h1');
+ const letters = Array.from(heading?.querySelectorAll('.hero-letter') || []);
const nav = document.querySelector('nav');
const brands = Array.from(document.querySelectorAll('.nav-brand'));
- if (!hero || !nav || !brands.length) return;
+ const navLetterGroups = brands.map(brand => Array.from(brand.querySelectorAll('.nav-brand-letter')));
+ if (!hero || !heading || !letters.length || !nav || !brands.length) return;
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
+ let heroTop = 0;
+ let fadeDistance = 1;
+ let headingSize = 1;
let updateFrame = 0;
- let brandIsVisible = null;
+ let resizeFrame = 0;
- function updateNavBrand() {
+ function clamp(value, minimum = 0, maximum = 1) {
+ return Math.min(maximum, Math.max(minimum, value));
+ }
+
+ function updateHeroLetters() {
updateFrame = 0;
- const revealBrand = hero.getBoundingClientRect().bottom <= nav.getBoundingClientRect().bottom + 1;
- if (revealBrand === brandIsVisible) return;
- brandIsVisible = revealBrand;
- brands.forEach(brand => brand.classList.toggle('is-visible', revealBrand));
+ if (reduceMotion.matches) {
+ const revealBrand = hero.getBoundingClientRect().bottom <= nav.getBoundingClientRect().bottom + 1;
+ letters.forEach(letter => {
+ letter.style.removeProperty('--hero-letter-opacity');
+ letter.style.removeProperty('--hero-letter-blur');
+ letter.style.removeProperty('--hero-letter-scale');
+ });
+ navLetterGroups.forEach(group => group.forEach(letter => {
+ letter.style.setProperty('--nav-letter-opacity', revealBrand ? '1' : '0');
+ letter.style.removeProperty('--nav-letter-blur');
+ letter.style.removeProperty('--nav-letter-x');
+ letter.style.removeProperty('--nav-letter-y');
+ letter.style.removeProperty('--nav-letter-rotate');
+ letter.style.removeProperty('--nav-letter-skew');
+ letter.style.removeProperty('--nav-letter-scale-x');
+ letter.style.removeProperty('--nav-letter-scale-y');
+ }));
+ brands.forEach(brand => brand.classList.toggle('is-visible', revealBrand));
+ return;
+ }
+
+ const progress = clamp((window.scrollY - heroTop) / fadeDistance);
+ let visibleNavLetters = 0;
+
+ letters.forEach((letter, index) => {
+ const start = 0.05 + index * 0.23;
+ const localProgress = clamp((progress - start) / 0.3);
+ const eased = localProgress * localProgress * (3 - 2 * localProgress);
+ const remaining = 1 - eased;
+ const wobble = Math.sin(localProgress * Math.PI * 3 + index * 0.65) * remaining;
+
+ letter.style.setProperty('--hero-letter-opacity', (1 - eased).toFixed(4));
+ letter.style.setProperty('--hero-letter-blur', `${(eased * headingSize * 0.018).toFixed(2)}px`);
+ letter.style.setProperty('--hero-letter-scale', (1 - eased * 0.055).toFixed(4));
+
+ navLetterGroups.forEach(group => {
+ const navLetter = group[index];
+ if (!navLetter) return;
+
+ navLetter.style.setProperty('--nav-letter-opacity', eased.toFixed(4));
+ navLetter.style.setProperty('--nav-letter-blur', `${(remaining * 0.32).toFixed(3)}rem`);
+ navLetter.style.setProperty('--nav-letter-x', `${(-1.35 * remaining + wobble * 0.42).toFixed(3)}rem`);
+ navLetter.style.setProperty('--nav-letter-y', `${(wobble * 0.18).toFixed(3)}rem`);
+ navLetter.style.setProperty('--nav-letter-rotate', `${(-12 * remaining + wobble * 7).toFixed(2)}deg`);
+ navLetter.style.setProperty('--nav-letter-skew', `${(-18 * remaining + wobble * 9).toFixed(2)}deg`);
+ navLetter.style.setProperty('--nav-letter-scale-x', `${(0.38 + eased * 0.62 + wobble * 0.08).toFixed(4)}`);
+ navLetter.style.setProperty('--nav-letter-scale-y', `${(1.28 - eased * 0.28 - wobble * 0.06).toFixed(4)}`);
+ });
+
+ if (eased > 0.02) visibleNavLetters += 1;
+ });
+
+ brands.forEach(brand => brand.classList.toggle('is-visible', visibleNavLetters > 0));
}
- function scheduleNavBrandUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateNavBrand);
+ function measureHeroLetters() {
+ resizeFrame = 0;
+ heroTop = hero.getBoundingClientRect().top + window.scrollY;
+ fadeDistance = Math.max(1, hero.offsetHeight - (nav?.getBoundingClientRect().height || 0));
+ headingSize = parseFloat(window.getComputedStyle(heading).fontSize) || 1;
+ updateHeroLetters();
}
+ function scheduleHeroLetterUpdate() {
+ if (!updateFrame) updateFrame = window.requestAnimationFrame(updateHeroLetters);
+ }
+
+ function scheduleHeroLetterMeasure() {
+ if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureHeroLetters);
+ }
+
+ window.addEventListener('scroll', scheduleHeroLetterUpdate, { passive: true });
+ window.addEventListener('resize', scheduleHeroLetterMeasure);
+ window.visualViewport?.addEventListener('resize', scheduleHeroLetterMeasure);
+ window.addEventListener('pageshow', scheduleHeroLetterMeasure);
+ reduceMotion.addEventListener('change', scheduleHeroLetterMeasure);
+ document.fonts?.ready.then(scheduleHeroLetterMeasure);
document.documentElement.classList.add('nav-brand-ready');
- window.addEventListener('scroll', scheduleNavBrandUpdate, { passive: true });
- window.addEventListener('resize', scheduleNavBrandUpdate);
- window.addEventListener('pageshow', scheduleNavBrandUpdate);
- updateNavBrand();
+ measureHeroLetters();
})();
(function () {
const intro = document.querySelector('.work-intro');
const stage = intro?.querySelector('.work-intro-stage');
+ const heading = intro?.querySelector('.work-heading');
+ const heroHeading = document.querySelector('.hero-content h1');
const words = Array.from(intro?.querySelectorAll('.work-heading-word') || []);
- if (!intro || !stage || !words.length) return;
+ if (!intro || !stage || !heading || !heroHeading || !words.length) return;
const nav = document.querySelector('nav');
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
let revealStart = 0;
let revealDistance = 1;
+ let shiftStart = 0;
+ let shiftEnd = 1;
+ let introStartShift = 0;
let revealedWords = -1;
let updateFrame = 0;
let resizeFrame = 0;
+ function clamp(value, minimum = 0, maximum = 1) {
+ return Math.min(maximum, Math.max(minimum, value));
+ }
+
+ function getDocumentTop(element) {
+ let top = 0;
+ let current = element;
+
+ while (current) {
+ top += current.offsetTop;
+ current = current.offsetParent;
+ }
+
+ return top;
+ }
+
function updateWorkIntro() {
updateFrame = 0;
if (reduceMotion.matches) {
+ stage.style.removeProperty('--work-intro-shift');
words.forEach(word => word.classList.add('is-revealed'));
revealedWords = words.length;
return;
}
- const progress = Math.min(1, Math.max(0, (window.scrollY - revealStart) / revealDistance));
+ const progress = window.scrollY <= 1
+ ? 0
+ : clamp((window.scrollY - revealStart) / revealDistance);
+ const shiftProgress = clamp((window.scrollY - shiftStart) / Math.max(1, shiftEnd - shiftStart));
+ stage.style.setProperty('--work-intro-shift', `${(introStartShift * (1 - shiftProgress)).toFixed(2)}px`);
const nextRevealedWords = Math.ceil(progress * words.length);
if (nextRevealedWords === revealedWords) return;
@@ -880,6 +1082,7 @@
function measureWorkIntro() {
resizeFrame = 0;
intro.classList.toggle('is-scroll-ready', !reduceMotion.matches);
+ intro.classList.remove('is-motion-ready');
const introTop = intro.getBoundingClientRect().top + window.scrollY;
const navHeight = nav?.getBoundingClientRect().height || 0;
@@ -889,8 +1092,21 @@
revealStart = introTop - Math.max(1, viewportHeight - navHeight);
revealDistance = Math.max(1, revealEnd - revealStart);
+ shiftStart = revealStart;
+ shiftEnd = introTop - navHeight;
+
+ const heroCenter = getDocumentTop(heroHeading) + heroHeading.offsetHeight / 2;
+ const introCenter = getDocumentTop(heading) + heading.offsetHeight / 2;
+ const availableShiftDistance = Math.max(1, shiftEnd - shiftStart) * 0.96;
+ introStartShift = Math.max(heroCenter - introCenter, -availableShiftDistance);
revealedWords = -1;
updateWorkIntro();
+
+ if (!reduceMotion.matches) {
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => intro.classList.add('is-motion-ready'));
+ });
+ }
}
function scheduleWorkIntroUpdate() {
@@ -915,7 +1131,7 @@
if (!cards.length) return;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- const touchLayout = window.matchMedia('(hover: none), (pointer: coarse)');
+ const mobileLayout = window.matchMedia('(max-width: 767px)');
let cardTops = [];
let updateFrame = 0;
let resizeFrame = 0;
@@ -938,7 +1154,7 @@
function fitDescriptions() {
cards.forEach(card => {
- const panel = card.querySelector(':scope > div');
+ const panel = card.querySelector(':scope > .portfolio-details');
const copy = panel?.querySelector('p');
if (!panel || !copy) return;
@@ -1014,22 +1230,30 @@
if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureCards);
}
+ function closeDescriptions(exceptCard = null) {
+ cards.forEach(card => {
+ if (card !== exceptCard) card.classList.remove('is-description-visible');
+ });
+ }
+
cards.forEach(card => {
- const link = card.querySelector(':scope > a');
- link?.addEventListener('click', event => {
- if (!touchLayout.matches || card.classList.contains('is-description-visible')) return;
+ card.addEventListener('click', event => {
+ if (!mobileLayout.matches || event.target.closest('.portfolio-live-link')) return;
- event.preventDefault();
- cards.forEach(otherCard => otherCard.classList.remove('is-description-visible'));
+ closeDescriptions(card);
card.classList.add('is-description-visible');
});
});
document.addEventListener('pointerdown', event => {
- if (!touchLayout.matches || event.target.closest('#work-scroller > article')) return;
- cards.forEach(card => card.classList.remove('is-description-visible'));
+ if (!mobileLayout.matches || event.target.closest('#work-scroller > article')) return;
+ closeDescriptions();
+ const activeCard = document.activeElement?.closest?.('#work-scroller > article');
+ if (activeCard) document.activeElement.blur();
}, { passive: true });
+ mobileLayout.addEventListener('change', () => closeDescriptions());
+
section.classList.add('portfolio-scroll-ready');
window.addEventListener('scroll', scheduleCardUpdate, { passive: true });
window.addEventListener('resize', scheduleCardMeasure);
@@ -1040,6 +1264,79 @@
measureCards();
})();
+ (function () {
+ const closingStatement = document.querySelector('.work-intro--closing');
+ const words = Array.from(closingStatement?.querySelectorAll('.work-heading-word') || []);
+ if (!closingStatement || !words.length) return;
+
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
+ let revealStart = 0;
+ let revealDistance = 1;
+ let revealedWords = -1;
+ let updateFrame = 0;
+ let resizeFrame = 0;
+
+ function clamp(value, minimum = 0, maximum = 1) {
+ return Math.min(maximum, Math.max(minimum, value));
+ }
+
+ function updateClosingIntro() {
+ updateFrame = 0;
+
+ if (reduceMotion.matches) {
+ words.forEach(word => word.classList.add('is-revealed'));
+ revealedWords = words.length;
+ return;
+ }
+
+ const progress = clamp((window.scrollY - revealStart) / revealDistance);
+ const nextRevealedWords = Math.ceil(progress * words.length);
+ if (nextRevealedWords === revealedWords) return;
+
+ revealedWords = nextRevealedWords;
+ words.forEach((word, index) => {
+ word.classList.toggle('is-revealed', index < revealedWords);
+ });
+ }
+
+ function measureClosingIntro() {
+ resizeFrame = 0;
+ closingStatement.classList.toggle('is-scroll-ready', !reduceMotion.matches);
+ closingStatement.classList.remove('is-motion-ready');
+
+ const statementTop = closingStatement.getBoundingClientRect().top + window.scrollY;
+ const viewportHeight = window.visualViewport?.height || window.innerHeight;
+ const statementHeight = closingStatement.offsetHeight;
+ const maximumScroll = Math.max(0, document.documentElement.scrollHeight - viewportHeight);
+ revealStart = Math.min(maximumScroll, statementTop - viewportHeight + statementHeight * 0.05);
+ revealDistance = Math.max(1, maximumScroll - revealStart);
+ revealedWords = -1;
+ updateClosingIntro();
+
+ if (!reduceMotion.matches) {
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => closingStatement.classList.add('is-motion-ready'));
+ });
+ }
+ }
+
+ function scheduleClosingIntroUpdate() {
+ if (!updateFrame) updateFrame = window.requestAnimationFrame(updateClosingIntro);
+ }
+
+ function scheduleClosingIntroMeasure() {
+ if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureClosingIntro);
+ }
+
+ window.addEventListener('scroll', scheduleClosingIntroUpdate, { passive: true });
+ window.addEventListener('resize', scheduleClosingIntroMeasure);
+ window.visualViewport?.addEventListener('resize', scheduleClosingIntroMeasure);
+ window.addEventListener('pageshow', scheduleClosingIntroMeasure);
+ reduceMotion.addEventListener('change', scheduleClosingIntroMeasure);
+ document.fonts?.ready.then(scheduleClosingIntroMeasure);
+ measureClosingIntro();
+ })();
+
</script>
</body>
</html>