patx/afterdarklabs
update
Commit 12b7f94 · patx · 2026-09-03T18:37:50-04:00
Comments
No comments yet.
Diff
diff --git a/docs/index.html b/docs/index.html
index 023c67b..31dd558 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -58,12 +58,18 @@
:root {
--neon: #ccff00;
- --page-bg: #17212b;
- --page-bg-rgb: 23, 33, 43;
+ --page-bg: #000000;
+ --page-bg-rgb: 0, 0, 0;
--site-nav-height: 5.75rem;
}
- html { scroll-behavior: smooth; }
+ html {
+ max-width: 100%;
+ overflow-x: hidden;
+ overflow-x: clip;
+ overscroll-behavior-x: none;
+ scroll-behavior: smooth;
+ }
@media (min-width: 1024px) {
:root {
@@ -74,8 +80,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;
}
@@ -92,12 +97,21 @@
}
body {
+ max-width: 100%;
+ overflow-x: hidden;
+ overflow-x: clip;
+ overscroll-behavior-x: none;
background: var(--page-bg);
font-family: 'Ubuntu', system-ui, sans-serif;
}
+ nav {
+ -webkit-backdrop-filter: none !important;
+ backdrop-filter: none !important;
+ }
+
.heading,
- .logo-font { font-family: inherit; }
+ .logo-font { font-family: inherit; color: #fff; }
.hero-bg { background: var(--page-bg); }
@@ -107,11 +121,11 @@
}
.neon-ink {
- color: var(--page-bg);
+ color: #fff;
}
.bg-black\/90 {
- background-color: rgba(var(--page-bg-rgb), 0.92) !important;
+ background-color: #000 !important;
}
.neon-text {
@@ -184,93 +198,110 @@
.hero-content {
position: relative;
z-index: 2;
+ max-width: none;
+ padding-inline: 0;
+ padding-block: 0;
}
- .hero-vignette {
- position: absolute;
- inset: 0;
- background:
- radial-gradient(ellipse 80% 70% at 50% 50%, transparent 42%, rgba(var(--page-bg-rgb),0.54) 100%),
- linear-gradient(to bottom, rgba(var(--page-bg-rgb),0.22) 0%, transparent 24%, transparent 78%, rgba(var(--page-bg-rgb),0.34) 100%);
- pointer-events: none;
- z-index: 1;
+ #hero {
+ box-sizing: border-box;
+ height: 100svh;
+ min-height: 100svh;
+ padding-top: var(--site-nav-height);
+ padding-bottom: 0;
}
- .hero-ticker {
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 3;
- overflow: hidden;
- border-top: 1px solid rgba(255,255,255,0.14);
- background: rgba(var(--page-bg-rgb),0.92);
- outline: none;
+ .nav-brand {
+ transform-origin: left center;
}
- .hero-ticker:focus-visible {
- border-top-color: var(--neon);
- box-shadow: inset 0 2px 0 var(--neon);
+ .nav-brand-ready .nav-brand {
+ visibility: hidden;
+ opacity: 1;
+ pointer-events: none;
+ transform: none;
}
- .hero-ticker-track {
- display: flex;
- width: max-content;
- animation: hero-ticker-scroll var(--ticker-duration, 10s) linear infinite;
- will-change: transform;
+ .nav-brand-ready .nav-brand.is-visible {
+ visibility: visible;
+ pointer-events: auto;
}
- .hero-ticker:focus-visible .hero-ticker-track {
- animation-play-state: paused;
+ .nav-cta {
+ display: inline-flex;
+ min-height: 3.25rem;
+ align-items: center;
+ justify-content: center;
+ padding: 0.75rem 1.15rem;
+ background-color: #000 !important;
+ color: #fff !important;
+ font-size: clamp(1.05rem, 5vw, 1.3rem);
+ line-height: 1;
+ letter-spacing: -0.035em;
}
- @media (hover: hover) and (pointer: fine) {
- .hero-ticker:hover .hero-ticker-track {
- animation-play-state: paused;
- }
+ .nav-cta:hover,
+ .nav-cta:focus-visible {
+ background-color: #fff !important;
+ color: #000 !important;
}
- .hero-ticker-group {
+ .nav-brand-word {
display: flex;
- flex: none;
- align-items: center;
- gap: 0.8rem;
- margin: 0;
- padding: 1.15rem 0.4rem 1.15rem 1.2rem;
- list-style: none;
+ gap: 0.22em;
+ padding-right: 0.08em;
+ font-size: clamp(1.25rem, 5.8vw, 1.7rem);
+ line-height: 0.9;
+ letter-spacing: -0.055em;
white-space: nowrap;
}
- .hero-ticker-item {
- display: inline-flex;
- align-items: center;
- gap: 0.8rem;
- color: #f3f4f6;
- font-size: 0.95rem;
- font-weight: 600;
- letter-spacing: 0;
+ .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;
}
- .hero-ticker-item::after {
- content: "";
- width: 0.35rem;
- height: 0.35rem;
- flex: none;
- border-radius: 50%;
- background: var(--neon);
- box-shadow: 0 0 0.5rem rgba(204,255,0,0.3);
+ .nav-brand-ready .nav-brand-letter {
+ opacity: var(--nav-letter-opacity, 0);
+ }
+
+ @media (min-width: 1024px) {
+ .nav-brand-word {
+ font-size: clamp(1.5rem, 2.2vw, 2rem);
+ }
+
+ .nav-cta {
+ min-height: 2.75rem;
+ padding: 0.72rem 1.35rem;
+ font-size: clamp(1.2rem, 2vw, 1.6rem);
+ }
+ }
+
+ .hero-vignette {
+ position: absolute;
+ inset: 0;
+ background: #000;
+ pointer-events: none;
+ z-index: 1;
}
@media (max-width: 767px) {
#hero {
--mobile-nav-height: 5.75rem;
- --mobile-ticker-height: 3.4rem;
box-sizing: border-box;
height: var(--mobile-hero-height, 100svh);
min-height: var(--mobile-hero-height, 100svh);
align-items: stretch;
padding-top: var(--mobile-nav-height);
- padding-bottom: var(--mobile-ticker-height);
+ padding-bottom: 0;
}
.hero-content {
@@ -284,14 +315,66 @@
font-size: clamp(2.75rem, min(17vw, 9.5svh), 4.5rem);
}
- .hero-ticker-group {
- padding-top: 0.95rem;
- padding-bottom: 0.95rem;
- }
}
- @keyframes hero-ticker-scroll {
- to { transform: translate3d(calc(0px - var(--ticker-distance, 50%)), 0, 0); }
+ .hero-content h1 {
+ width: max-content;
+ padding-right: 0.1em;
+ font-size: 53vw;
+ font-size: min(53vw, calc((100svh - var(--site-nav-height)) * 1.25));
+ line-height: 0.8;
+ letter-spacing: -0.075em;
+ }
+
+ .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;
+ }
+
+ .hero-scroll-hint {
+ position: absolute;
+ bottom: max(1rem, env(safe-area-inset-bottom));
+ left: 50%;
+ z-index: 3;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55rem;
+ color: #fff;
+ font-size: clamp(0.7rem, 1.1vw, 0.85rem);
+ font-weight: 500;
+ line-height: 1;
+ letter-spacing: 0.08em;
+ text-decoration: none;
+ text-transform: uppercase;
+ transform: translateX(-50%);
+ transition: opacity 160ms ease;
+ }
+
+ .hero-scroll-hint::after {
+ width: 0.58em;
+ height: 0.58em;
+ border-right: 0.12em solid currentColor;
+ border-bottom: 0.12em solid currentColor;
+ content: "";
+ animation: hero-scroll-nudge 1.65s cubic-bezier(0.45, 0, 0.2, 1) infinite;
+ }
+
+ .hero-scroll-hint:hover {
+ opacity: 0.68;
+ }
+
+ .hero-scroll-hint:focus-visible {
+ outline: 2px solid var(--neon);
+ outline-offset: 0.4rem;
+ }
+
+ @keyframes hero-scroll-nudge {
+ 0%, 100% { transform: translateY(-0.12rem) rotate(45deg); }
+ 50% { transform: translateY(0.2rem) rotate(45deg); }
}
@keyframes neon-letter-flicker {
@@ -318,74 +401,260 @@
}
}
+ .work-intro {
+ position: relative;
+ height: 100svh;
+ background: var(--page-bg);
+ }
+
+ .work-intro:not(.work-intro--closing) {
+ height: auto;
+ }
+
+ .work-intro-stage {
+ position: sticky;
+ top: var(--site-nav-height);
+ z-index: 3;
+ display: flex;
+ box-sizing: border-box;
+ height: calc(100svh - var(--site-nav-height));
+ align-items: center;
+ overflow: hidden;
+ padding: clamp(0.75rem, 1.5vw, 1.25rem) clamp(1.5rem, 5vw, 4rem);
+ }
+
+ .work-intro:not(.work-intro--closing) .work-intro-stage {
+ position: relative;
+ top: auto;
+ height: auto;
+ align-items: flex-start;
+ padding-top: 0;
+ padding-bottom: 25px;
+ }
+
+ .work-heading {
+ display: flex;
+ width: 100%;
+ max-width: 78rem;
+ flex-wrap: wrap;
+ gap: 0 0.2em;
+ margin: 0 auto;
+ color: #fff;
+ font-size: clamp(2.85rem, 8vw, 7rem);
+ font-weight: 700;
+ line-height: 0.94;
+ letter-spacing: -0.065em;
+ text-wrap: balance;
+ }
+
+ #work-heading {
+ scroll-margin-top: calc(var(--site-nav-height) + 1rem);
+ }
+
+ .work-heading-words {
+ display: contents;
+ }
+
+ .work-heading-word {
+ display: inline-block;
+ }
+
+ .work-heading-email {
+ color: inherit;
+ font-weight: 500;
+ text-decoration: none;
+ transition: font-weight 160ms ease;
+ }
+
+ .work-heading-email .bi-send {
+ display: inline-block;
+ width: 0.9em;
+ height: 0.9em;
+ vertical-align: -0.06em;
+ }
+
+ .work-heading-email:hover,
+ .work-heading-email:focus-visible {
+ font-weight: 700;
+ }
+
+ .work-heading-email:focus-visible {
+ outline: 0.055em solid #fff;
+ outline-offset: 0.075em;
+ }
+
+ .work-intro--closing {
+ height: auto;
+ min-height: 0;
+ }
+
+ .work-intro--closing .work-intro-stage {
+ position: relative;
+ top: auto;
+ height: auto;
+ align-items: flex-start;
+ padding-top: 48px;
+ padding-bottom: 56px;
+ }
+
+ .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);
+ }
+
+ .work-intro.is-scroll-ready .work-heading-word.is-revealed {
+ opacity: 1;
+ filter: none;
+ transform: none;
+ }
+
@media (prefers-reduced-motion: reduce) {
+ .nav-brand-letter {
+ filter: none;
+ transform: none;
+ will-change: auto;
+ }
+
.neon-letter,
.neon-letter--flicker {
animation: none;
}
- .hero-ticker {
- overflow-x: auto;
+ .hero-letter {
+ opacity: 1;
+ filter: none;
+ transform: none;
+ will-change: auto;
}
- .hero-ticker-track {
+ .hero-scroll-hint::after {
animation: none;
+ }
+
+ .work-intro {
+ height: auto;
+ }
+
+ .work-intro-stage {
+ position: relative;
+ top: auto;
+ height: auto;
transform: none;
will-change: auto;
}
- .hero-ticker-group[aria-hidden="true"] {
- display: none;
+ .work-intro.is-scroll-ready .work-heading-word {
+ opacity: 1;
+ filter: none;
+ transform: none;
+ transition: none;
}
}
.work-stage {
- padding-block: 5rem 5.5rem;
- }
-
- .work-scroll-ready .work-stage {
- position: sticky;
- top: 0;
- display: flex;
- height: 100svh;
- min-height: 34rem;
- align-items: center;
- overflow: hidden;
box-sizing: border-box;
- padding-top: clamp(6rem, 12svh, 8rem);
- padding-bottom: clamp(2.5rem, 8svh, 5rem);
+ padding: clamp(0rem, 0.5vw, 0.5rem) clamp(1rem, 2vw, 2rem);
}
.work-stage-content {
width: 100%;
+ max-width: none;
min-width: 0;
+ margin-inline: auto;
}
#work-scroller {
- padding-inline: 1rem 0;
- scroll-padding-inline: 1rem;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: clamp(0.3rem, 0.6vw, 0.65rem);
+ overflow: visible;
+ padding: 0;
+ touch-action: pan-y pinch-zoom;
}
- #work-scroller > article:last-child {
- margin-inline-end: 1rem;
+ #work-scroller > article {
+ position: relative;
+ isolation: isolate;
+ width: 100%;
+ min-width: 0;
+ aspect-ratio: 16 / 9;
+ border-radius: 0;
+ }
+
+ #work-scroller > article > .portfolio-media {
+ display: block;
+ width: 100%;
+ height: 100%;
+ border-radius: 0;
+ }
+
+ #work-scroller > article > .portfolio-media > img {
+ width: 100%;
+ height: 100%;
+ aspect-ratio: auto;
+ object-fit: cover;
+ transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
- .portfolio-cover {
+ #work-scroller > article > .portfolio-details {
+ position: absolute;
+ inset: 0;
+ z-index: 2;
display: flex;
- width: fit-content;
+ box-sizing: border-box;
align-items: center;
- border: 1px solid var(--page-bg);
- background: var(--page-bg);
- color: #fff;
+ overflow: hidden;
+ padding: clamp(1rem, 2.5vw, 2.75rem);
+ background: rgba(0, 0, 0, 0.94);
+ opacity: 0;
+ clip-path: inset(100% 0 0 0);
+ pointer-events: none;
+ transform: translate3d(0, 7%, 0) skewY(1.2deg);
+ transform-origin: center bottom;
+ transition:
+ opacity 180ms ease,
+ clip-path 480ms cubic-bezier(0.16, 1, 0.3, 1),
+ transform 520ms cubic-bezier(0.16, 1, 0.3, 1);
+ backdrop-filter: blur(0.35rem);
}
- .portfolio-cover-title {
- padding-block: clamp(1.5rem, 4vw, 3rem);
- padding-inline: 1rem;
- font-size: clamp(2.75rem, 6vw, 4rem);
- font-weight: 700;
- line-height: 0.86;
- letter-spacing: -0.07em;
+ #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:focus-within > .portfolio-media > img,
+ #work-scroller > article.is-description-visible > .portfolio-media > img {
+ transform: scale(1.025);
+ }
+
+ #work-scroller > article:focus-within {
+ outline: 2px solid var(--neon);
+ outline-offset: -2px;
+ }
+
+ #work-scroller > article > .portfolio-details > p {
+ width: 100%;
+ margin: 0;
+ color: #fff;
+ font-size: clamp(1.2rem, 3vw, 3.5rem);
+ font-weight: 500;
+ line-height: 0.98;
+ letter-spacing: -0.04em;
+ text-wrap: balance;
}
.portfolio-description {
@@ -397,70 +666,102 @@
font-weight: 700;
}
- .work-scroll-ready #work-scroller {
- overflow: visible;
- scroll-behavior: auto;
- touch-action: pan-y;
- transform: translate3d(var(--work-offset, 0px), 0, 0);
- will-change: transform;
+ .portfolio-live-link {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 0.18em;
+ color: #fff;
+ font-weight: 500;
+ text-decoration: none;
+ text-shadow: 0 0 currentColor;
+ transition: text-shadow 160ms ease;
+ white-space: nowrap;
+ }
+
+ .portfolio-live-link svg {
+ width: 0.72em;
+ height: 0.72em;
+ flex: none;
+ }
+
+ .portfolio-live-link:hover,
+ .portfolio-live-link:focus-visible {
+ font-weight: 500;
+ text-shadow:
+ 0.018em 0 currentColor,
+ -0.018em 0 currentColor;
+ }
+
+ .portfolio-live-link:focus-visible {
+ outline: 0.09em solid #fff;
+ outline-offset: 0.08em;
+ }
+
+ .portfolio-scroll-ready #work-scroller > article {
+ opacity: var(--card-opacity, 0);
+ filter: blur(var(--card-blur, 0.4rem));
+ transform:
+ translate3d(var(--card-x, 0), var(--card-y, 5rem), 0)
+ rotate(var(--card-rotate, 0deg))
+ skewY(var(--card-skew, 0deg))
+ scale(var(--card-scale-x, 0.94), var(--card-scale-y, 1.04));
+ transform-origin: center bottom;
+ will-change: auto;
+ }
+
+ .portfolio-scroll-ready #work-scroller > article.is-card-animating {
+ will-change: opacity, filter, transform;
}
@media (min-width: 768px) {
- .work-scroll-ready .work-stage {
- top: var(--site-nav-height);
- height: calc(100svh - var(--site-nav-height));
- min-height: 0;
- padding-block: clamp(1rem, 3svh, 2rem);
+ #work-scroller {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
}
- .work-scroll-ready #work-scroller {
- padding-bottom: 0;
- }
+ }
- #work-scroller > article {
- width: clamp(25rem, calc(125svh - 12rem), 38rem);
+ @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 > .portfolio-cover {
- width: fit-content;
+ #work-scroller > article:hover > .portfolio-media > img {
+ transform: scale(1.025);
}
}
- @media (prefers-reduced-motion: reduce) {
- .work-scroll-ready {
- height: auto !important;
+ @media (hover: none), (pointer: coarse) {
+ #work-scroller > article {
+ cursor: pointer;
}
- .work-scroll-ready .work-stage {
- position: relative;
- top: auto;
- height: auto;
- min-height: 0;
- overflow: visible;
- padding-block: 5rem 5.5rem;
+ #work-scroller > article.is-description-visible {
+ cursor: default;
}
- .work-scroll-ready #work-scroller {
- display: grid;
- width: auto;
- grid-template-columns: 1fr;
- overflow: visible;
- transform: none !important;
- will-change: auto;
+ .portfolio-scroll-ready #work-scroller > article {
+ filter: none;
}
- .work-scroll-ready #work-scroller > article {
- width: auto;
+ #work-scroller > article > .portfolio-details {
+ -webkit-backdrop-filter: none;
+ backdrop-filter: none;
}
+ }
- #work-scroller > article:last-child {
- margin-inline-end: 0;
+ @media (prefers-reduced-motion: reduce) {
+ .portfolio-scroll-ready #work-scroller > article {
+ opacity: 1;
+ filter: none;
+ transform: none;
}
- }
- @media (min-width: 768px) and (prefers-reduced-motion: reduce) {
- .work-scroll-ready #work-scroller {
- grid-template-columns: repeat(2, minmax(0, 1fr));
+ #work-scroller > article > .portfolio-media > img,
+ #work-scroller > article > .portfolio-details {
+ transition: none;
}
}
@@ -528,25 +829,19 @@
<nav class="fixed top-0 z-50 w-full bg-black/90 backdrop-blur-lg">
<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="flex min-w-0 items-center gap-2 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">
- <img src="https://afterdarklabs.io/logo_assets/mark.svg" width="48" height="48" alt="" aria-hidden="true">
- <span class="min-w-0">
- <span class="logo-font block truncate text-xl font-semibold tracking-tighter">After Dark</span>
- <span class="logo-font -mt-1 block text-xs tracking-[3px] text-[#ccff00]">LABS</span>
- </span>
+ <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="After Dark Labs — go to top">
+ <span class="nav-brand-word logo-font font-semibold" aria-hidden="true"><span class="nav-brand-letter">After</span><span class="nav-brand-letter">Dark</span><span class="nav-brand-letter">Labs</span></span>
</a>
- <a href="mailto:[email protected]" class="neon-ink shrink-0 rounded-2xl bg-[#ccff00] 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="flex items-center gap-3" aria-label="Go to top">
- <img src="https://afterdarklabs.io/logo_assets/mark.svg" width="48" height="48" alt="" aria-hidden="true">
+ <a href="#hero" class="nav-brand flex items-center gap-3" aria-label="After Dark Labs — go to top">
<div>
- <span class="logo-font block text-2xl font-semibold tracking-tighter">After Dark</span>
- <span class="logo-font -mt-1 block text-sm tracking-[3px] text-[#ccff00]">LABS</span>
+ <span class="nav-brand-word logo-font font-semibold" aria-hidden="true"><span class="nav-brand-letter">After</span><span class="nav-brand-letter">Dark</span><span class="nav-brand-letter">Labs</span></span>
</div>
</a>
- <a href="mailto:[email protected]" class="neon-ink rounded-2xl bg-[#ccff00] 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>
@@ -557,151 +852,155 @@
<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">
- Web design that <span class="neon-text" aria-hidden="true"><span class="neon-letter neon-letter--flicker" data-letter="s" style="--flicker-delay:-1.1s; --flicker-duration:11s;">s</span><span class="neon-letter" data-letter="t">t</span><span class="neon-letter neon-letter--flicker" data-letter="a" style="--flicker-delay:-5.6s; --flicker-duration:13s;">a</span><span class="neon-letter" data-letter="n">n</span><span class="neon-letter neon-letter--flicker" data-letter="d" style="--flicker-delay:-8.4s; --flicker-duration:15s;">d</span><span class="neon-letter neon-letter--flicker" data-letter="s" style="--flicker-delay:-7.4s; --flicker-duration:10s;">s</span><span class="neon-gap"></span><span class="neon-letter" data-letter="o">o</span><span class="neon-letter neon-letter--flicker" data-letter="u" style="--flicker-delay:-3.3s; --flicker-duration:12s;">u</span><span class="neon-letter" data-letter="t">t</span></span><span class="sr-only">stand out</span>
- </h1>
+ <h1 class="heading text-7xl font-bold leading-none tracking-tighter md:text-8xl lg:text-7xl xl:text-8xl" aria-label="ADL"><span class="hero-letter" aria-hidden="true">A</span><span class="hero-letter" aria-hidden="true">D</span><span class="hero-letter" aria-hidden="true">L</span></h1>
</div>
</div>
- <div class="hero-ticker" role="region" aria-label="Services" tabindex="0">
- <div class="hero-ticker-track">
- <ul class="hero-ticker-group" aria-label="Services offered">
- <li class="hero-ticker-item">Custom Websites & Apps</li>
- <li class="hero-ticker-item">Ecommerce Stores</li>
- <li class="hero-ticker-item">Brand Identity</li>
- <li class="hero-ticker-item">AI Integrations</li>
- </ul>
- <ul class="hero-ticker-group" aria-hidden="true">
- <li class="hero-ticker-item">Custom Websites & Apps</li>
- <li class="hero-ticker-item">Ecommerce Stores</li>
- <li class="hero-ticker-item">Brand Identity</li>
- <li class="hero-ticker-item">AI Integrations</li>
- </ul>
- </div>
+ <a class="hero-scroll-hint" href="#work-heading">Scroll</a>
+
+ </section>
+
+ <section class="work-intro" aria-labelledby="work-heading">
+ <div class="work-intro-stage">
+ <h2 id="work-heading" class="work-heading" aria-label="We build custom websites and apps. Take a look at some of our work.">
+ <span class="work-heading-words" aria-hidden="true">
+ <span class="work-heading-word">We</span>
+ <span class="work-heading-word">build</span>
+ <span class="work-heading-word">custom</span>
+ <span class="work-heading-word">websites</span>
+ <span class="work-heading-word">and</span>
+ <span class="work-heading-word">apps.</span>
+ <span class="work-heading-word">Take</span>
+ <span class="work-heading-word">a</span>
+ <span class="work-heading-word">look</span>
+ <span class="work-heading-word">at</span>
+ <span class="work-heading-word">some</span>
+ <span class="work-heading-word">of</span>
+ <span class="work-heading-word">our</span>
+ <span class="work-heading-word">work.</span>
+ </span>
+ </h2>
</div>
</section>
<section id="work" class="scroll-mt-24 border-t border-white/10 bg-black" aria-label="Featured work">
<div class="work-stage">
<div class="work-stage-content">
- <div id="work-scroller" class="no-scrollbar flex snap-x snap-mandatory gap-5 overflow-x-auto scroll-smooth pb-2">
- <article class="portfolio-cover w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl sm:w-[28rem] md:w-[38rem]">
- <p class="portfolio-cover-title">Featured<br>Work <span class="neon-text" aria-hidden="true"><span class="neon-letter neon-letter--flicker" data-letter="→" style="--flicker-delay:-4.2s; --flicker-duration:11s;">→</span></span></p>
- </article>
+ <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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">View site <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-box-arrow-up-right" viewBox="0 0 16 16" aria-hidden="true"><path fill-rule="evenodd" d="M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5"/><path fill-rule="evenodd" d="M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0z"/></svg></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">
+ <a class="work-heading-word work-heading-email" href="mailto:[email protected]">
+ <span class="work-heading-word">Let’s</span>
+ <span class="work-heading-word">move</span>
+ <span class="work-heading-word">your</span>
+ <span class="work-heading-word">business</span>
+ <span class="work-heading-word">forward</span>
+ <span class="work-heading-word">
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-send" viewBox="0 0 16 16">
+ <path d="M15.854.146a.5.5 0 0 1 .11.54l-5.819 14.547a.75.75 0 0 1-1.329.124l-3.178-4.995L.643 7.184a.75.75 0 0 1 .124-1.33L15.314.037a.5.5 0 0 1 .54.11ZM6.636 10.07l2.761 4.338L14.13 2.576zm6.787-8.201L1.591 6.602l4.339 2.76z"/>
+ </svg>
+ </span>
+ </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">
- <img src="https://afterdarklabs.io/logo_assets/mark-animated.svg" width="48" height="48" alt="" aria-hidden="true">
- <span class="min-w-0 text-left">
- <span class="logo-font block text-3xl font-semibold tracking-tighter">After Dark</span>
- <span class="logo-font -mt-1 block text-sm tracking-[3px] text-[#ccff00]">LABS</span>
- </span>
- </a>
- <p class="text-white">
- <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');
if (!hero) return;
const nav = document.querySelector('nav');
- const ticker = hero.querySelector('.hero-ticker');
const mobileLayout = window.matchMedia('(max-width: 767px)');
let lockedWidth = 0;
let resizeFrame = 0;
@@ -712,15 +1011,12 @@
if (!mobileLayout.matches) {
hero.style.removeProperty('--mobile-hero-height');
hero.style.removeProperty('--mobile-nav-height');
- hero.style.removeProperty('--mobile-ticker-height');
lockedWidth = 0;
return;
}
const navHeight = Math.round(nav?.getBoundingClientRect().height || 0);
- const tickerHeight = Math.round(ticker?.getBoundingClientRect().height || 0);
if (navHeight) hero.style.setProperty('--mobile-nav-height', `${navHeight}px`);
- if (tickerHeight) hero.style.setProperty('--mobile-ticker-height', `${tickerHeight}px`);
const layoutWidth = Math.round(document.documentElement.clientWidth);
if (Math.abs(layoutWidth - lockedWidth) < 2) return;
@@ -743,114 +1039,464 @@
})();
(function () {
- const ticker = document.querySelector('.hero-ticker');
- const track = ticker?.querySelector('.hero-ticker-track');
- const source = track?.querySelector('.hero-ticker-group');
- if (!ticker || !track || !source) return;
+ const hero = document.getElementById('hero');
+ const heading = hero?.querySelector('.hero-content h1');
+ const letters = Array.from(heading?.querySelectorAll('.hero-letter') || []);
+ const scrollHint = hero?.querySelector('.hero-scroll-hint');
+ const nav = document.querySelector('nav');
+ const brands = Array.from(document.querySelectorAll('.nav-brand'));
+ 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 resizeFrame = 0;
+
+ function clamp(value, minimum = 0, maximum = 1) {
+ return Math.min(maximum, Math.max(minimum, value));
+ }
+
+ function updateHeroLetters() {
+ updateFrame = 0;
+
+ 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));
+ if (scrollHint) {
+ scrollHint.style.opacity = revealBrand ? '0' : '1';
+ scrollHint.style.pointerEvents = revealBrand ? 'none' : 'auto';
+ scrollHint.toggleAttribute('inert', 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));
+
+ if (scrollHint && index === letters.length - 1) {
+ const hintVisible = remaining > 0.02;
+ scrollHint.style.opacity = remaining.toFixed(4);
+ scrollHint.style.pointerEvents = hintVisible ? 'auto' : 'none';
+ scrollHint.toggleAttribute('inert', !hintVisible);
+ }
+
+ 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 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');
+ measureHeroLetters();
+ })();
+
+ (function () {
+ const intro = document.querySelector('.work-intro:not(.work-intro--closing)');
+ const heading = intro?.querySelector('.work-heading');
+ const firstCard = document.querySelector('#work-scroller > article');
+ const words = Array.from(intro?.querySelectorAll('.work-heading-word') || []);
+ if (!intro || !heading || !words.length) return;
+
+ const nav = document.querySelector('nav');
+ 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));
+ }
- let measureFrame = 0;
+ function getDocumentTop(element) {
+ let top = 0;
+ let current = element;
- function measureTicker() {
- measureFrame = 0;
+ while (current) {
+ top += current.offsetTop;
+ current = current.offsetParent;
+ }
- const groups = Array.from(track.querySelectorAll('.hero-ticker-group'));
- groups.slice(1).forEach(group => group.remove());
+ return top;
+ }
- const groupWidth = source.getBoundingClientRect().width;
- if (!groupWidth) return;
+ function updateWorkIntro() {
+ updateFrame = 0;
- const copiesNeeded = Math.max(2, Math.ceil(ticker.clientWidth / groupWidth) + 1);
- for (let index = 1; index < copiesNeeded; index += 1) {
- const clone = source.cloneNode(true);
- clone.removeAttribute('aria-label');
- clone.setAttribute('aria-hidden', 'true');
- track.appendChild(clone);
+ if (reduceMotion.matches) {
+ words.forEach(word => word.classList.add('is-revealed'));
+ revealedWords = words.length;
+ return;
}
- track.style.setProperty('--ticker-distance', `${groupWidth}px`);
- track.style.setProperty('--ticker-duration', `${groupWidth / 60}s`);
+ const progress = window.scrollY <= 1
+ ? 0
+ : 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 measureWorkIntro() {
+ resizeFrame = 0;
+ intro.classList.toggle('is-scroll-ready', !reduceMotion.matches);
+ intro.classList.remove('is-motion-ready');
+
+ const navHeight = nav?.getBoundingClientRect().height || 0;
+ const viewportHeight = window.visualViewport?.height || window.innerHeight;
+ const headingCenter = getDocumentTop(heading) + heading.offsetHeight / 2;
+ const visualCenter = navHeight + (viewportHeight - navHeight) * 0.58;
+ const cardRevealStart = firstCard
+ ? getDocumentTop(firstCard) - viewportHeight * 0.94
+ : headingCenter - visualCenter + viewportHeight * 0.32;
+ revealStart = headingCenter - visualCenter;
+ const revealEnd = Math.max(
+ cardRevealStart + viewportHeight * 0.06,
+ revealStart + viewportHeight * 0.24
+ );
+ revealDistance = Math.max(1, revealEnd - revealStart);
+ revealedWords = -1;
+ updateWorkIntro();
+
+ if (!reduceMotion.matches) {
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => intro.classList.add('is-motion-ready'));
+ });
+ }
}
- function scheduleTickerMeasure() {
- if (!measureFrame) measureFrame = window.requestAnimationFrame(measureTicker);
+ function scheduleWorkIntroUpdate() {
+ if (!updateFrame) updateFrame = window.requestAnimationFrame(updateWorkIntro);
}
- window.addEventListener('resize', scheduleTickerMeasure);
- document.fonts?.ready.then(scheduleTickerMeasure);
- measureTicker();
+ function scheduleWorkIntroMeasure() {
+ if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureWorkIntro);
+ }
+
+ window.addEventListener('scroll', scheduleWorkIntroUpdate, { passive: true });
+ window.addEventListener('resize', scheduleWorkIntroMeasure);
+ window.addEventListener('pageshow', scheduleWorkIntroMeasure);
+ reduceMotion.addEventListener('change', scheduleWorkIntroMeasure);
+ document.fonts?.ready.then(scheduleWorkIntroMeasure);
+ measureWorkIntro();
})();
(function () {
const section = document.getElementById('work');
- const stage = section?.querySelector('.work-stage');
- const scroller = document.getElementById('work-scroller');
- if (!section || !stage || !scroller) return;
-
- const cards = Array.from(scroller.querySelectorAll(':scope > article'));
+ const cards = Array.from(section?.querySelectorAll('#work-scroller > article') || []);
if (!cards.length) return;
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- let scrollDistance = 0;
- let sectionTop = 0;
+ const touchDisclosure = window.matchMedia('(hover: none), (pointer: coarse)');
+ let cardTops = [];
+ let cardStates = cards.map(() => -1);
+ let layoutWidth = 0;
+ let layoutHeight = 0;
+ let lastScrollY = -1;
let updateFrame = 0;
let resizeFrame = 0;
- let keyboardNavigation = false;
-
- function updateWorkPosition() {
- updateFrame = 0;
- if (reduceMotion.matches || !scrollDistance) return;
- const progress = Math.min(1, Math.max(0, (window.scrollY - sectionTop) / scrollDistance));
- scroller.style.setProperty('--work-offset', `${-(scrollDistance * progress)}px`);
+ function clamp(value, minimum = 0, maximum = 1) {
+ return Math.min(maximum, Math.max(minimum, value));
}
- function scheduleWorkUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateWorkPosition);
+ function getDocumentTop(element) {
+ let top = 0;
+ let current = element;
+
+ while (current) {
+ top += current.offsetTop;
+ current = current.offsetParent;
+ }
+
+ return top;
}
- function measureWorkSection() {
- resizeFrame = 0;
- section.classList.add('work-scroll-ready');
+ function fitDescriptions() {
+ cards.forEach(card => {
+ const panel = card.querySelector(':scope > .portfolio-details');
+ const copy = panel?.querySelector('p');
+ if (!panel || !copy) return;
+
+ const panelStyle = window.getComputedStyle(panel);
+ const horizontalPadding = parseFloat(panelStyle.paddingLeft) + parseFloat(panelStyle.paddingRight);
+ const verticalPadding = parseFloat(panelStyle.paddingTop) + parseFloat(panelStyle.paddingBottom);
+ const availableWidth = Math.max(1, panel.clientWidth - horizontalPadding);
+ const availableHeight = Math.max(1, panel.clientHeight - verticalPadding);
+ let minimum = 12;
+ let maximum = Math.min(88, Math.max(34, card.clientWidth / 7.2));
+
+ for (let iteration = 0; iteration < 10; iteration += 1) {
+ const size = (minimum + maximum) / 2;
+ copy.style.fontSize = `${size}px`;
+
+ if (copy.scrollHeight <= availableHeight + 1 && copy.scrollWidth <= availableWidth + 1) {
+ minimum = size;
+ } else {
+ maximum = size;
+ }
+ }
+
+ copy.style.fontSize = `${Math.floor(minimum * 10) / 10}px`;
+ });
+ }
+ function updateCards() {
+ updateFrame = 0;
if (reduceMotion.matches) {
- section.style.removeProperty('height');
- scroller.style.removeProperty('--work-offset');
- scrollDistance = 0;
+ cards.forEach(card => card.classList.remove('is-card-animating'));
return;
}
- scrollDistance = Math.max(0, scroller.scrollWidth - stage.clientWidth);
- section.style.height = `${Math.ceil(window.innerHeight + scrollDistance)}px`;
- sectionTop = section.getBoundingClientRect().top + window.scrollY;
- updateWorkPosition();
+ const scrollY = Math.max(0, window.scrollY);
+ const viewportHeight = layoutHeight || document.documentElement.clientHeight || window.innerHeight;
+ if (scrollY === lastScrollY) return;
+
+ lastScrollY = scrollY;
+ const revealDistance = Math.max(240, viewportHeight * 0.58);
+ const travelX = Math.min(72, document.documentElement.clientWidth * 0.06);
+
+ cards.forEach((card, index) => {
+ const progress = clamp((scrollY + viewportHeight * 0.94 - cardTops[index]) / revealDistance);
+ const state = progress <= 0 ? 0 : progress >= 1 ? 1 : 2;
+ if (cardStates[index] !== state) {
+ card.classList.toggle('is-card-animating', state === 2);
+ }
+ if (state !== 2 && cardStates[index] === state) return;
+
+ cardStates[index] = state;
+ const eased = 1 - Math.pow(1 - progress, 3);
+ const remaining = 1 - eased;
+ const direction = index % 2 === 0 ? -1 : 1;
+ const wave = Math.sin(progress * Math.PI * 3) * remaining;
+ const x = direction * (remaining * travelX + wave * 20);
+ const y = remaining * 96 - Math.sin(progress * Math.PI) * 10;
+ const rotation = direction * (remaining * 5 + wave * 2.4);
+ const skew = direction * wave * 1.6;
+ const scaleX = 1 - remaining * 0.07 + Math.abs(wave) * 0.025;
+ const scaleY = 1 + remaining * 0.065 - wave * 0.02;
+ const opacity = clamp(progress * 1.65);
+ const blur = remaining * 7;
+
+ card.style.setProperty('--card-x', `${x.toFixed(2)}px`);
+ card.style.setProperty('--card-y', `${y.toFixed(2)}px`);
+ card.style.setProperty('--card-rotate', `${rotation.toFixed(2)}deg`);
+ card.style.setProperty('--card-skew', `${skew.toFixed(2)}deg`);
+ card.style.setProperty('--card-scale-x', scaleX.toFixed(4));
+ card.style.setProperty('--card-scale-y', scaleY.toFixed(4));
+ card.style.setProperty('--card-opacity', opacity.toFixed(4));
+ card.style.setProperty('--card-blur', `${blur.toFixed(2)}px`);
+ });
+ }
+
+ function measureCards() {
+ resizeFrame = 0;
+ const nextLayoutWidth = document.documentElement.clientWidth;
+ const nextLayoutHeight = document.documentElement.clientHeight || window.innerHeight;
+ const widthChanged = Math.abs(nextLayoutWidth - layoutWidth) >= 2;
+ const desktopHeightChanged = !touchDisclosure.matches && Math.abs(nextLayoutHeight - layoutHeight) >= 2;
+ if (layoutWidth && !widthChanged && !desktopHeightChanged) {
+ scheduleCardUpdate();
+ return;
+ }
+
+ layoutWidth = nextLayoutWidth;
+ layoutHeight = nextLayoutHeight;
+ cardTops = cards.map(getDocumentTop);
+ cardStates = cards.map(() => -1);
+ lastScrollY = -1;
+ fitDescriptions();
+ updateCards();
+ }
+
+ function scheduleCardUpdate() {
+ if (!updateFrame) updateFrame = window.requestAnimationFrame(updateCards);
+ }
+
+ function scheduleCardMeasure() {
+ if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureCards);
}
- function scheduleWorkMeasure() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureWorkSection);
+ function scheduleCardRemeasure() {
+ layoutWidth = 0;
+ scheduleCardMeasure();
}
- document.addEventListener('keydown', event => {
- if (event.key === 'Tab') keyboardNavigation = true;
+ function closeDescriptions(exceptCard = null) {
+ cards.forEach(card => {
+ if (card !== exceptCard) card.classList.remove('is-description-visible');
+ });
+ }
+
+ cards.forEach(card => {
+ card.addEventListener('click', event => {
+ if (!touchDisclosure.matches || event.target.closest('.portfolio-live-link')) return;
+
+ closeDescriptions(card);
+ card.classList.add('is-description-visible');
+ });
});
- document.addEventListener('pointerdown', () => {
- keyboardNavigation = false;
+
+ document.addEventListener('pointerdown', event => {
+ if (!touchDisclosure.matches || event.target.closest('#work-scroller > article')) return;
+ closeDescriptions();
+ const activeCard = document.activeElement?.closest?.('#work-scroller > article');
+ if (activeCard) document.activeElement.blur();
}, { passive: true });
- scroller.addEventListener('focusin', event => {
- if (!keyboardNavigation || reduceMotion.matches) return;
+ touchDisclosure.addEventListener('change', () => closeDescriptions());
+
+ section.classList.add('portfolio-scroll-ready');
+ window.addEventListener('scroll', scheduleCardUpdate, { passive: true });
+ window.addEventListener('resize', scheduleCardMeasure);
+ window.visualViewport?.addEventListener('resize', scheduleCardMeasure);
+ window.addEventListener('pageshow', scheduleCardRemeasure);
+ reduceMotion.addEventListener('change', scheduleCardRemeasure);
+ document.fonts?.ready.then(scheduleCardRemeasure);
+ measureCards();
+ })();
- const card = event.target.closest('article');
- if (!card) return;
+ (function () {
+ const closingStatement = document.querySelector('.work-intro--closing');
+ const words = Array.from(closingStatement?.querySelectorAll('.work-heading-word') || []);
+ if (!closingStatement || !words.length) return;
- const target = Math.min(card.offsetLeft - cards[0].offsetLeft, scrollDistance);
- window.scrollTo({ top: sectionTop + Math.max(0, target), behavior: 'auto' });
- });
+ 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', scheduleWorkUpdate, { passive: true });
- window.addEventListener('resize', scheduleWorkMeasure);
- window.addEventListener('load', scheduleWorkMeasure);
- reduceMotion.addEventListener('change', scheduleWorkMeasure);
- measureWorkSection();
+ 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>
diff --git a/docs/index_firstlove.html b/docs/index_firstlove.html
new file mode 100644
index 0000000..023c67b
--- /dev/null
+++ b/docs/index_firstlove.html
@@ -0,0 +1,858 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <link rel="icon" href="https://afterdarklabs.io/logo_assets/favicon.ico">
+ <link rel="icon" type="image/svg+xml" href="https://afterdarklabs.io/logo_assets/badge.svg">
+ <link rel="icon" type="image/png" sizes="32x32" href="https://afterdarklabs.io/logo_assets/favicon-32.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="https://afterdarklabs.io/logo_assets/favicon-16.png">
+ <link rel="apple-touch-icon" sizes="180x180" href="https://afterdarklabs.io/logo_assets/apple-touch-icon.png">
+ <link rel="manifest" href="https://afterdarklabs.io/site.webmanifest">
+ <title>Website Design & Development in South Florida | After Dark Labs</title>
+ <meta name="description" content="South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, and business tools.">
+ <meta name="robots" content="index,follow,max-image-preview:large">
+ <link rel="canonical" href="https://afterdarklabs.io/">
+ <meta property="og:title" content="Website Design & Development in South Florida | After Dark Labs">
+ <meta property="og:description" content="South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, and business tools.">
+ <meta property="og:image" content="https://afterdarklabs.io/twitter-card.png?v=3">
+ <meta property="og:image:width" content="1200">
+ <meta property="og:image:height" content="630">
+ <meta property="og:image:alt" content="After Dark Labs websites and tools that help businesses stand out">
+ <meta property="og:type" content="website">
+ <meta property="og:url" content="https://afterdarklabs.io/">
+ <meta property="og:site_name" content="After Dark Labs">
+ <meta property="og:locale" content="en_US">
+ <meta name="twitter:card" content="summary_large_image">
+ <meta name="twitter:title" content="Website Design & Development in South Florida | After Dark Labs">
+ <meta name="twitter:description" content="South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, and business tools.">
+ <meta name="twitter:image" content="https://afterdarklabs.io/twitter-card.png?v=3">
+ <meta name="twitter:image:alt" content="After Dark Labs websites and tools that help businesses stand out">
+ <link rel="preload" href="https://afterdarklabs.io/fonts/ubuntu-400-latin.woff2" as="font" type="font/woff2" crossorigin>
+ <link rel="preload" href="https://afterdarklabs.io/fonts/ubuntu-700-latin.woff2" as="font" type="font/woff2" crossorigin>
+ <link rel="stylesheet" href="https://afterdarklabs.io/index.tailwind.css?v=4">
+ <style>
+ @font-face {
+ font-family: 'Ubuntu';
+ font-style: normal;
+ font-weight: 400;
+ font-display: swap;
+ src: url('https://afterdarklabs.io/fonts/ubuntu-400-latin.woff2') format('woff2');
+ }
+
+ @font-face {
+ font-family: 'Ubuntu';
+ font-style: normal;
+ font-weight: 500;
+ font-display: swap;
+ src: url('https://afterdarklabs.io/fonts/ubuntu-500-latin.woff2') format('woff2');
+ }
+
+ @font-face {
+ font-family: 'Ubuntu';
+ font-style: normal;
+ font-weight: 700;
+ font-display: swap;
+ src: url('https://afterdarklabs.io/fonts/ubuntu-700-latin.woff2') format('woff2');
+ }
+
+ :root {
+ --neon: #ccff00;
+ --page-bg: #17212b;
+ --page-bg-rgb: 23, 33, 43;
+ --site-nav-height: 5.75rem;
+ }
+
+ html { scroll-behavior: smooth; }
+
+ @media (min-width: 1024px) {
+ :root {
+ --site-nav-height: 5.5rem;
+ }
+
+ html {
+ font-size: 125%;
+ }
+
+ nav img[src*="/logo_assets/mark"],
+ footer img[src*="/logo_assets/mark"] {
+ width: 3rem;
+ height: 3rem;
+ }
+
+ .hero-content h1 {
+ font-size: 4.5rem;
+ }
+ }
+
+ @media (min-width: 1600px) {
+ .hero-content h1 {
+ font-size: 6rem;
+ }
+ }
+
+ body {
+ background: var(--page-bg);
+ font-family: 'Ubuntu', system-ui, sans-serif;
+ }
+
+ .heading,
+ .logo-font { font-family: inherit; }
+
+ .hero-bg { background: var(--page-bg); }
+
+ .bg-black,
+ .bg-zinc-950 {
+ background-color: var(--page-bg) !important;
+ }
+
+ .neon-ink {
+ color: var(--page-bg);
+ }
+
+ .bg-black\/90 {
+ background-color: rgba(var(--page-bg-rgb), 0.92) !important;
+ }
+
+ .neon-text {
+ position: relative;
+ isolation: isolate;
+ display: inline-flex;
+ align-items: baseline;
+ font: inherit;
+ letter-spacing: inherit;
+ line-height: inherit;
+ color: var(--neon);
+ white-space: nowrap;
+ }
+
+ .neon-text::before {
+ content: "";
+ position: absolute;
+ inset: -0.24em -0.42em -0.18em;
+ background: radial-gradient(ellipse at 50% 58%, rgba(204,255,0,0.38), rgba(204,255,0,0.16) 42%, transparent 74%);
+ filter: blur(0.18em);
+ opacity: 0.9;
+ pointer-events: none;
+ z-index: -2;
+ }
+
+ .neon-letter,
+ .neon-gap {
+ position: relative;
+ display: inline-block;
+ font: inherit;
+ letter-spacing: inherit;
+ line-height: inherit;
+ }
+
+ .neon-letter {
+ color: #ccff00;
+ -webkit-text-fill-color: currentColor;
+ -webkit-text-stroke: 0.006em rgba(235,255,120,0.58);
+ filter:
+ drop-shadow(0 0 0.012em rgba(235,255,120,0.54))
+ drop-shadow(0 0 0.045em rgba(204,255,0,0.72))
+ drop-shadow(0 0 0.105em rgba(204,255,0,0.28))
+ drop-shadow(0 0 0.24em rgba(204,255,0,0.16));
+ text-shadow:
+ 0 0 0.026em rgba(235,255,120,0.38),
+ 0 0 0.095em rgba(204,255,0,0.42);
+ }
+
+ .neon-letter::after {
+ content: attr(data-letter);
+ position: absolute;
+ inset: 0;
+ color: transparent;
+ -webkit-text-fill-color: transparent;
+ -webkit-text-stroke: 0.018em rgba(4,8,10,0.46);
+ transform: translate(0.01em, 0.012em);
+ opacity: 0.45;
+ z-index: -1;
+ }
+
+ .neon-letter--flicker {
+ animation: neon-letter-flicker var(--flicker-duration, 12s) steps(1, end) infinite;
+ animation-delay: var(--flicker-delay, 0s);
+ }
+
+ .neon-gap {
+ width: 0.18em;
+ }
+
+ .hero-content {
+ position: relative;
+ z-index: 2;
+ }
+
+ .hero-vignette {
+ position: absolute;
+ inset: 0;
+ background:
+ radial-gradient(ellipse 80% 70% at 50% 50%, transparent 42%, rgba(var(--page-bg-rgb),0.54) 100%),
+ linear-gradient(to bottom, rgba(var(--page-bg-rgb),0.22) 0%, transparent 24%, transparent 78%, rgba(var(--page-bg-rgb),0.34) 100%);
+ pointer-events: none;
+ z-index: 1;
+ }
+
+ .hero-ticker {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 3;
+ overflow: hidden;
+ border-top: 1px solid rgba(255,255,255,0.14);
+ background: rgba(var(--page-bg-rgb),0.92);
+ outline: none;
+ }
+
+ .hero-ticker:focus-visible {
+ border-top-color: var(--neon);
+ box-shadow: inset 0 2px 0 var(--neon);
+ }
+
+ .hero-ticker-track {
+ display: flex;
+ width: max-content;
+ animation: hero-ticker-scroll var(--ticker-duration, 10s) linear infinite;
+ will-change: transform;
+ }
+
+ .hero-ticker:focus-visible .hero-ticker-track {
+ animation-play-state: paused;
+ }
+
+ @media (hover: hover) and (pointer: fine) {
+ .hero-ticker:hover .hero-ticker-track {
+ animation-play-state: paused;
+ }
+ }
+
+ .hero-ticker-group {
+ display: flex;
+ flex: none;
+ align-items: center;
+ gap: 0.8rem;
+ margin: 0;
+ padding: 1.15rem 0.4rem 1.15rem 1.2rem;
+ list-style: none;
+ white-space: nowrap;
+ }
+
+ .hero-ticker-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.8rem;
+ color: #f3f4f6;
+ font-size: 0.95rem;
+ font-weight: 600;
+ letter-spacing: 0;
+ }
+
+ .hero-ticker-item::after {
+ content: "";
+ width: 0.35rem;
+ height: 0.35rem;
+ flex: none;
+ border-radius: 50%;
+ background: var(--neon);
+ box-shadow: 0 0 0.5rem rgba(204,255,0,0.3);
+ }
+
+ @media (max-width: 767px) {
+ #hero {
+ --mobile-nav-height: 5.75rem;
+ --mobile-ticker-height: 3.4rem;
+ box-sizing: border-box;
+ height: var(--mobile-hero-height, 100svh);
+ min-height: var(--mobile-hero-height, 100svh);
+ align-items: stretch;
+ padding-top: var(--mobile-nav-height);
+ padding-bottom: var(--mobile-ticker-height);
+ }
+
+ .hero-content {
+ align-content: center;
+ gap: 0;
+ padding-block: clamp(1.25rem, 4svh, 2rem);
+ }
+
+ .hero-content h1 {
+ font-size: clamp(2.75rem, 17vw, 4.5rem);
+ font-size: clamp(2.75rem, min(17vw, 9.5svh), 4.5rem);
+ }
+
+ .hero-ticker-group {
+ padding-top: 0.95rem;
+ padding-bottom: 0.95rem;
+ }
+ }
+
+ @keyframes hero-ticker-scroll {
+ to { transform: translate3d(calc(0px - var(--ticker-distance, 50%)), 0, 0); }
+ }
+
+ @keyframes neon-letter-flicker {
+ 0%, 4.8%, 5.4%, 5.9%, 37.5%, 38%, 38.6%, 100% {
+ opacity: 1;
+ -webkit-text-stroke-color: rgba(235,255,120,0.58);
+ filter:
+ drop-shadow(0 0 0.012em rgba(235,255,120,0.54))
+ drop-shadow(0 0 0.045em rgba(204,255,0,0.72))
+ drop-shadow(0 0 0.105em rgba(204,255,0,0.28))
+ drop-shadow(0 0 0.24em rgba(204,255,0,0.16));
+ }
+ 5%, 37.8% {
+ opacity: 0.08;
+ -webkit-text-stroke-color: rgba(204,255,0,0.18);
+ filter: none;
+ }
+ 5.7%, 38.3% {
+ opacity: 0.38;
+ -webkit-text-stroke-color: rgba(204,255,0,0.36);
+ filter:
+ drop-shadow(0 0 0.012em rgba(204,255,0,0.34))
+ drop-shadow(0 0 0.036em rgba(204,255,0,0.20));
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .neon-letter,
+ .neon-letter--flicker {
+ animation: none;
+ }
+
+ .hero-ticker {
+ overflow-x: auto;
+ }
+
+ .hero-ticker-track {
+ animation: none;
+ transform: none;
+ will-change: auto;
+ }
+
+ .hero-ticker-group[aria-hidden="true"] {
+ display: none;
+ }
+ }
+
+ .work-stage {
+ padding-block: 5rem 5.5rem;
+ }
+
+ .work-scroll-ready .work-stage {
+ position: sticky;
+ top: 0;
+ display: flex;
+ height: 100svh;
+ min-height: 34rem;
+ align-items: center;
+ overflow: hidden;
+ box-sizing: border-box;
+ padding-top: clamp(6rem, 12svh, 8rem);
+ padding-bottom: clamp(2.5rem, 8svh, 5rem);
+ }
+
+ .work-stage-content {
+ width: 100%;
+ min-width: 0;
+ }
+
+ #work-scroller {
+ padding-inline: 1rem 0;
+ scroll-padding-inline: 1rem;
+ }
+
+ #work-scroller > article:last-child {
+ margin-inline-end: 1rem;
+ }
+
+ .portfolio-cover {
+ display: flex;
+ width: fit-content;
+ align-items: center;
+ border: 1px solid var(--page-bg);
+ background: var(--page-bg);
+ color: #fff;
+ }
+
+ .portfolio-cover-title {
+ padding-block: clamp(1.5rem, 4vw, 3rem);
+ padding-inline: 1rem;
+ font-size: clamp(2.75rem, 6vw, 4rem);
+ font-weight: 700;
+ line-height: 0.86;
+ letter-spacing: -0.07em;
+ }
+
+ .portfolio-description {
+ display: block;
+ color: #fff;
+ }
+
+ .portfolio-name {
+ font-weight: 700;
+ }
+
+ .work-scroll-ready #work-scroller {
+ overflow: visible;
+ scroll-behavior: auto;
+ touch-action: pan-y;
+ transform: translate3d(var(--work-offset, 0px), 0, 0);
+ will-change: transform;
+ }
+
+ @media (min-width: 768px) {
+ .work-scroll-ready .work-stage {
+ top: var(--site-nav-height);
+ height: calc(100svh - var(--site-nav-height));
+ min-height: 0;
+ padding-block: clamp(1rem, 3svh, 2rem);
+ }
+
+ .work-scroll-ready #work-scroller {
+ padding-bottom: 0;
+ }
+
+ #work-scroller > article {
+ width: clamp(25rem, calc(125svh - 12rem), 38rem);
+ }
+
+ #work-scroller > .portfolio-cover {
+ width: fit-content;
+ }
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .work-scroll-ready {
+ height: auto !important;
+ }
+
+ .work-scroll-ready .work-stage {
+ position: relative;
+ top: auto;
+ height: auto;
+ min-height: 0;
+ overflow: visible;
+ padding-block: 5rem 5.5rem;
+ }
+
+ .work-scroll-ready #work-scroller {
+ display: grid;
+ width: auto;
+ grid-template-columns: 1fr;
+ overflow: visible;
+ transform: none !important;
+ will-change: auto;
+ }
+
+ .work-scroll-ready #work-scroller > article {
+ width: auto;
+ }
+
+ #work-scroller > article:last-child {
+ margin-inline-end: 0;
+ }
+ }
+
+ @media (min-width: 768px) and (prefers-reduced-motion: reduce) {
+ .work-scroll-ready #work-scroller {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ }
+
+ </style>
+ <script type="application/ld+json">
+ {
+ "@context": "https://schema.org",
+ "@graph": [
+ {
+ "@type": "Organization",
+ "@id": "https://afterdarklabs.io/#organization",
+ "name": "After Dark Labs",
+ "url": "https://afterdarklabs.io/",
+ "logo": "https://afterdarklabs.io/logo_assets/icon-512.png",
+ "founder": {
+ "@type": "Person",
+ "name": "Harrison Erd",
+ "url": "https://harrisonerd.com/"
+ },
+ "areaServed": {
+ "@type": "Country",
+ "name": "United States"
+ },
+ "description": "After Dark Labs provides South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, branding, hosting, and business tools."
+ },
+ {
+ "@type": "WebSite",
+ "@id": "https://afterdarklabs.io/#website",
+ "url": "https://afterdarklabs.io/",
+ "name": "After Dark Labs",
+ "alternateName": "After Dark Labs Florida",
+ "publisher": {
+ "@id": "https://afterdarklabs.io/#organization"
+ },
+ "description": "After Dark Labs provides website design and development for custom websites, Shopify stores, mobile apps, and business tools."
+ },
+ {
+ "@type": "Service",
+ "@id": "https://afterdarklabs.io/#digital-services",
+ "name": "Website, ecommerce, app, and business software development",
+ "description": "Custom websites, ecommerce stores, mobile apps, SaaS platforms, branding, hosting, APIs, AI chatbots, and intelligent automation.",
+ "provider": {
+ "@id": "https://afterdarklabs.io/#organization"
+ },
+ "areaServed": {
+ "@type": "Country",
+ "name": "United States"
+ },
+ "serviceType": [
+ "Custom website design and development",
+ "Shopify and ecommerce development",
+ "Mobile app development",
+ "Custom platforms, dashboards, and API integrations",
+ "Brand identity and digital marketing",
+ "Hosting and business technology",
+ "AI integrations, chatbots, and intelligent automation"
+ ],
+ "url": "https://afterdarklabs.io/"
+ }
+ ]
+ }
+ </script>
+</head>
+<body class="bg-black text-white overflow-x-hidden">
+ <nav class="fixed top-0 z-50 w-full bg-black/90 backdrop-blur-lg">
+ <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="flex min-w-0 items-center gap-2 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">
+ <img src="https://afterdarklabs.io/logo_assets/mark.svg" width="48" height="48" alt="" aria-hidden="true">
+ <span class="min-w-0">
+ <span class="logo-font block truncate text-xl font-semibold tracking-tighter">After Dark</span>
+ <span class="logo-font -mt-1 block text-xs tracking-[3px] text-[#ccff00]">LABS</span>
+ </span>
+ </a>
+ <a href="mailto:[email protected]" class="neon-ink shrink-0 rounded-2xl bg-[#ccff00] px-4 py-3 text-sm 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="flex items-center gap-3" aria-label="Go to top">
+ <img src="https://afterdarklabs.io/logo_assets/mark.svg" width="48" height="48" alt="" aria-hidden="true">
+ <div>
+ <span class="logo-font block text-2xl font-semibold tracking-tighter">After Dark</span>
+ <span class="logo-font -mt-1 block text-sm tracking-[3px] text-[#ccff00]">LABS</span>
+ </div>
+ </a>
+ <a href="mailto:[email protected]" class="neon-ink rounded-2xl bg-[#ccff00] px-6 py-3 font-semibold transition-colors hover:bg-white">Let's Talk</a>
+ </div>
+ </div>
+ </nav>
+
+ <main>
+ <section id="hero" class="hero-bg min-h-screen flex items-center pt-20" style="position:relative; overflow:hidden;">
+ <div class="hero-vignette"></div>
+
+ <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">
+ Web design that <span class="neon-text" aria-hidden="true"><span class="neon-letter neon-letter--flicker" data-letter="s" style="--flicker-delay:-1.1s; --flicker-duration:11s;">s</span><span class="neon-letter" data-letter="t">t</span><span class="neon-letter neon-letter--flicker" data-letter="a" style="--flicker-delay:-5.6s; --flicker-duration:13s;">a</span><span class="neon-letter" data-letter="n">n</span><span class="neon-letter neon-letter--flicker" data-letter="d" style="--flicker-delay:-8.4s; --flicker-duration:15s;">d</span><span class="neon-letter neon-letter--flicker" data-letter="s" style="--flicker-delay:-7.4s; --flicker-duration:10s;">s</span><span class="neon-gap"></span><span class="neon-letter" data-letter="o">o</span><span class="neon-letter neon-letter--flicker" data-letter="u" style="--flicker-delay:-3.3s; --flicker-duration:12s;">u</span><span class="neon-letter" data-letter="t">t</span></span><span class="sr-only">stand out</span>
+ </h1>
+ </div>
+ </div>
+
+ <div class="hero-ticker" role="region" aria-label="Services" tabindex="0">
+ <div class="hero-ticker-track">
+ <ul class="hero-ticker-group" aria-label="Services offered">
+ <li class="hero-ticker-item">Custom Websites & Apps</li>
+ <li class="hero-ticker-item">Ecommerce Stores</li>
+ <li class="hero-ticker-item">Brand Identity</li>
+ <li class="hero-ticker-item">AI Integrations</li>
+ </ul>
+ <ul class="hero-ticker-group" aria-hidden="true">
+ <li class="hero-ticker-item">Custom Websites & Apps</li>
+ <li class="hero-ticker-item">Ecommerce Stores</li>
+ <li class="hero-ticker-item">Brand Identity</li>
+ <li class="hero-ticker-item">AI Integrations</li>
+ </ul>
+ </div>
+ </div>
+ </section>
+
+ <section id="work" class="scroll-mt-24 border-t border-white/10 bg-black" aria-label="Featured work">
+ <div class="work-stage">
+ <div class="work-stage-content">
+ <div id="work-scroller" class="no-scrollbar flex snap-x snap-mandatory gap-5 overflow-x-auto scroll-smooth pb-2">
+ <article class="portfolio-cover w-[82vw] shrink-0 snap-start overflow-hidden rounded-3xl sm:w-[28rem] md:w-[38rem]">
+ <p class="portfolio-cover-title">Featured<br>Work <span class="neon-text" aria-hidden="true"><span class="neon-letter neon-letter--flicker" data-letter="→" style="--flicker-delay:-4.2s; --flicker-duration:11s;">→</span></span></p>
+ </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://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">
+ <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">
+ <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>
+ </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">
+ <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">
+ <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>
+ </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">
+ <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">
+ <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>
+ </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">
+ <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">
+ <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>
+ </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">
+ <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">
+ <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>
+ </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">
+ <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">
+ <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>
+ </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">
+ <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">
+ <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>
+ </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">
+ <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">
+ <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>
+ </p>
+ </div>
+ </article>
+ </div>
+ </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">
+ <img src="https://afterdarklabs.io/logo_assets/mark-animated.svg" width="48" height="48" alt="" aria-hidden="true">
+ <span class="min-w-0 text-left">
+ <span class="logo-font block text-3xl font-semibold tracking-tighter">After Dark</span>
+ <span class="logo-font -mt-1 block text-sm tracking-[3px] text-[#ccff00]">LABS</span>
+ </span>
+ </a>
+ <p class="text-white">
+ <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');
+ if (!hero) return;
+
+ const nav = document.querySelector('nav');
+ const ticker = hero.querySelector('.hero-ticker');
+ const mobileLayout = window.matchMedia('(max-width: 767px)');
+ let lockedWidth = 0;
+ let resizeFrame = 0;
+
+ function lockMobileHeroHeight() {
+ resizeFrame = 0;
+
+ if (!mobileLayout.matches) {
+ hero.style.removeProperty('--mobile-hero-height');
+ hero.style.removeProperty('--mobile-nav-height');
+ hero.style.removeProperty('--mobile-ticker-height');
+ lockedWidth = 0;
+ return;
+ }
+
+ const navHeight = Math.round(nav?.getBoundingClientRect().height || 0);
+ const tickerHeight = Math.round(ticker?.getBoundingClientRect().height || 0);
+ if (navHeight) hero.style.setProperty('--mobile-nav-height', `${navHeight}px`);
+ if (tickerHeight) hero.style.setProperty('--mobile-ticker-height', `${tickerHeight}px`);
+
+ const layoutWidth = Math.round(document.documentElement.clientWidth);
+ if (Math.abs(layoutWidth - lockedWidth) < 2) return;
+
+ const visibleHeight = Math.round(window.visualViewport?.height || window.innerHeight);
+ if (!visibleHeight) return;
+
+ lockedWidth = layoutWidth;
+ hero.style.setProperty('--mobile-hero-height', `${visibleHeight}px`);
+ }
+
+ function scheduleHeroHeightLock() {
+ if (!resizeFrame) resizeFrame = window.requestAnimationFrame(lockMobileHeroHeight);
+ }
+
+ window.addEventListener('resize', scheduleHeroHeightLock);
+ window.visualViewport?.addEventListener('resize', scheduleHeroHeightLock);
+ mobileLayout.addEventListener?.('change', scheduleHeroHeightLock);
+ lockMobileHeroHeight();
+ })();
+
+ (function () {
+ const ticker = document.querySelector('.hero-ticker');
+ const track = ticker?.querySelector('.hero-ticker-track');
+ const source = track?.querySelector('.hero-ticker-group');
+ if (!ticker || !track || !source) return;
+
+ let measureFrame = 0;
+
+ function measureTicker() {
+ measureFrame = 0;
+
+ const groups = Array.from(track.querySelectorAll('.hero-ticker-group'));
+ groups.slice(1).forEach(group => group.remove());
+
+ const groupWidth = source.getBoundingClientRect().width;
+ if (!groupWidth) return;
+
+ const copiesNeeded = Math.max(2, Math.ceil(ticker.clientWidth / groupWidth) + 1);
+ for (let index = 1; index < copiesNeeded; index += 1) {
+ const clone = source.cloneNode(true);
+ clone.removeAttribute('aria-label');
+ clone.setAttribute('aria-hidden', 'true');
+ track.appendChild(clone);
+ }
+
+ track.style.setProperty('--ticker-distance', `${groupWidth}px`);
+ track.style.setProperty('--ticker-duration', `${groupWidth / 60}s`);
+ }
+
+ function scheduleTickerMeasure() {
+ if (!measureFrame) measureFrame = window.requestAnimationFrame(measureTicker);
+ }
+
+ window.addEventListener('resize', scheduleTickerMeasure);
+ document.fonts?.ready.then(scheduleTickerMeasure);
+ measureTicker();
+ })();
+
+ (function () {
+ const section = document.getElementById('work');
+ const stage = section?.querySelector('.work-stage');
+ const scroller = document.getElementById('work-scroller');
+ if (!section || !stage || !scroller) return;
+
+ const cards = Array.from(scroller.querySelectorAll(':scope > article'));
+ if (!cards.length) return;
+
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
+ let scrollDistance = 0;
+ let sectionTop = 0;
+ let updateFrame = 0;
+ let resizeFrame = 0;
+ let keyboardNavigation = false;
+
+ function updateWorkPosition() {
+ updateFrame = 0;
+ if (reduceMotion.matches || !scrollDistance) return;
+
+ const progress = Math.min(1, Math.max(0, (window.scrollY - sectionTop) / scrollDistance));
+ scroller.style.setProperty('--work-offset', `${-(scrollDistance * progress)}px`);
+ }
+
+ function scheduleWorkUpdate() {
+ if (!updateFrame) updateFrame = window.requestAnimationFrame(updateWorkPosition);
+ }
+
+ function measureWorkSection() {
+ resizeFrame = 0;
+ section.classList.add('work-scroll-ready');
+
+ if (reduceMotion.matches) {
+ section.style.removeProperty('height');
+ scroller.style.removeProperty('--work-offset');
+ scrollDistance = 0;
+ return;
+ }
+
+ scrollDistance = Math.max(0, scroller.scrollWidth - stage.clientWidth);
+ section.style.height = `${Math.ceil(window.innerHeight + scrollDistance)}px`;
+ sectionTop = section.getBoundingClientRect().top + window.scrollY;
+ updateWorkPosition();
+ }
+
+ function scheduleWorkMeasure() {
+ if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureWorkSection);
+ }
+
+ document.addEventListener('keydown', event => {
+ if (event.key === 'Tab') keyboardNavigation = true;
+ });
+ document.addEventListener('pointerdown', () => {
+ keyboardNavigation = false;
+ }, { passive: true });
+
+ scroller.addEventListener('focusin', event => {
+ if (!keyboardNavigation || reduceMotion.matches) return;
+
+ const card = event.target.closest('article');
+ if (!card) return;
+
+ const target = Math.min(card.offsetLeft - cards[0].offsetLeft, scrollDistance);
+ window.scrollTo({ top: sectionTop + Math.max(0, target), behavior: 'auto' });
+ });
+
+ window.addEventListener('scroll', scheduleWorkUpdate, { passive: true });
+ window.addEventListener('resize', scheduleWorkMeasure);
+ window.addEventListener('load', scheduleWorkMeasure);
+ reduceMotion.addEventListener('change', scheduleWorkMeasure);
+ measureWorkSection();
+ })();
+
+ </script>
+</body>
+</html>
diff --git a/docs/indexerd.html b/docs/indexerd.html
deleted file mode 100644
index aa55ade..0000000
--- a/docs/indexerd.html
+++ /dev/null
@@ -1,1496 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link rel="icon" href="https://afterdarklabs.io/logo_assets/favicon.ico">
- <link rel="icon" type="image/svg+xml" href="https://afterdarklabs.io/logo_assets/badge.svg">
- <link rel="icon" type="image/png" sizes="32x32" href="https://afterdarklabs.io/logo_assets/favicon-32.png">
- <link rel="icon" type="image/png" sizes="16x16" href="https://afterdarklabs.io/logo_assets/favicon-16.png">
- <link rel="apple-touch-icon" sizes="180x180" href="https://afterdarklabs.io/logo_assets/apple-touch-icon.png">
- <link rel="manifest" href="https://afterdarklabs.io/site.webmanifest">
- <title>Website Design & Development in South Florida | After Dark Labs</title>
- <meta name="description" content="South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, and business tools.">
- <meta name="robots" content="index,follow,max-image-preview:large">
- <link rel="canonical" href="https://afterdarklabs.io/">
- <meta property="og:title" content="Website Design & Development in South Florida | After Dark Labs">
- <meta property="og:description" content="South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, and business tools.">
- <meta property="og:image" content="https://afterdarklabs.io/twitter-card.png?v=3">
- <meta property="og:image:width" content="1200">
- <meta property="og:image:height" content="630">
- <meta property="og:image:alt" content="After Dark Labs websites and tools that help businesses stand out">
- <meta property="og:type" content="website">
- <meta property="og:url" content="https://afterdarklabs.io/">
- <meta property="og:site_name" content="After Dark Labs">
- <meta property="og:locale" content="en_US">
- <meta name="twitter:card" content="summary_large_image">
- <meta name="twitter:title" content="Website Design & Development in South Florida | After Dark Labs">
- <meta name="twitter:description" content="South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, and business tools.">
- <meta name="twitter:image" content="https://afterdarklabs.io/twitter-card.png?v=3">
- <meta name="twitter:image:alt" content="After Dark Labs websites and tools that help businesses stand out">
- <link rel="preload" href="https://afterdarklabs.io/fonts/ubuntu-400-latin.woff2" as="font" type="font/woff2" crossorigin>
- <link rel="preload" href="https://afterdarklabs.io/fonts/ubuntu-700-latin.woff2" as="font" type="font/woff2" crossorigin>
- <link rel="stylesheet" href="https://afterdarklabs.io/index.tailwind.css?v=4">
- <style>
- @font-face {
- font-family: 'Ubuntu';
- font-style: normal;
- font-weight: 400;
- font-display: swap;
- src: url('https://afterdarklabs.io/fonts/ubuntu-400-latin.woff2') format('woff2');
- }
-
- @font-face {
- font-family: 'Ubuntu';
- font-style: normal;
- font-weight: 500;
- font-display: swap;
- src: url('https://afterdarklabs.io/fonts/ubuntu-500-latin.woff2') format('woff2');
- }
-
- @font-face {
- font-family: 'Ubuntu';
- font-style: normal;
- font-weight: 700;
- font-display: swap;
- src: url('https://afterdarklabs.io/fonts/ubuntu-700-latin.woff2') format('woff2');
- }
-
- :root {
- --neon: #ccff00;
- --page-bg: #000000;
- --page-bg-rgb: 0, 0, 0;
- --site-nav-height: 5.75rem;
- }
-
- html {
- max-width: 100%;
- overflow-x: hidden;
- overflow-x: clip;
- overscroll-behavior-x: none;
- scroll-behavior: smooth;
- }
-
- @media (min-width: 1024px) {
- :root {
- --site-nav-height: 5.5rem;
- }
-
- html {
- font-size: 125%;
- }
-
- nav img[src*="/logo_assets/mark"] {
- width: 3rem;
- height: 3rem;
- }
-
- .hero-content h1 {
- font-size: 4.5rem;
- }
- }
-
- @media (min-width: 1600px) {
- .hero-content h1 {
- font-size: 6rem;
- }
- }
-
- body {
- max-width: 100%;
- overflow-x: hidden;
- overflow-x: clip;
- overscroll-behavior-x: none;
- background: var(--page-bg);
- font-family: 'Ubuntu', system-ui, sans-serif;
- }
-
- nav {
- -webkit-backdrop-filter: none !important;
- backdrop-filter: none !important;
- }
-
- .heading,
- .logo-font { font-family: inherit; color: #fff; }
-
- .hero-bg { background: var(--page-bg); }
-
- .bg-black,
- .bg-zinc-950 {
- background-color: var(--page-bg) !important;
- }
-
- .neon-ink {
- color: #fff;
- }
-
- .bg-black\/90 {
- background-color: #000 !important;
- }
-
- .neon-text {
- position: relative;
- isolation: isolate;
- display: inline-flex;
- align-items: baseline;
- font: inherit;
- letter-spacing: inherit;
- line-height: inherit;
- color: var(--neon);
- white-space: nowrap;
- }
-
- .neon-text::before {
- content: "";
- position: absolute;
- inset: -0.24em -0.42em -0.18em;
- background: radial-gradient(ellipse at 50% 58%, rgba(204,255,0,0.38), rgba(204,255,0,0.16) 42%, transparent 74%);
- filter: blur(0.18em);
- opacity: 0.9;
- pointer-events: none;
- z-index: -2;
- }
-
- .neon-letter,
- .neon-gap {
- position: relative;
- display: inline-block;
- font: inherit;
- letter-spacing: inherit;
- line-height: inherit;
- }
-
- .neon-letter {
- color: #ccff00;
- -webkit-text-fill-color: currentColor;
- -webkit-text-stroke: 0.006em rgba(235,255,120,0.58);
- filter:
- drop-shadow(0 0 0.012em rgba(235,255,120,0.54))
- drop-shadow(0 0 0.045em rgba(204,255,0,0.72))
- drop-shadow(0 0 0.105em rgba(204,255,0,0.28))
- drop-shadow(0 0 0.24em rgba(204,255,0,0.16));
- text-shadow:
- 0 0 0.026em rgba(235,255,120,0.38),
- 0 0 0.095em rgba(204,255,0,0.42);
- }
-
- .neon-letter::after {
- content: attr(data-letter);
- position: absolute;
- inset: 0;
- color: transparent;
- -webkit-text-fill-color: transparent;
- -webkit-text-stroke: 0.018em rgba(4,8,10,0.46);
- transform: translate(0.01em, 0.012em);
- opacity: 0.45;
- z-index: -1;
- }
-
- .neon-letter--flicker {
- animation: neon-letter-flicker var(--flicker-duration, 12s) steps(1, end) infinite;
- animation-delay: var(--flicker-delay, 0s);
- }
-
- .neon-gap {
- width: 0.18em;
- }
-
- .hero-content {
- position: relative;
- z-index: 2;
- max-width: none;
- padding-inline: 0;
- padding-block: 0;
- }
-
- #hero {
- box-sizing: border-box;
- height: 100svh;
- min-height: 100svh;
- padding-top: var(--site-nav-height);
- padding-bottom: 0;
- }
-
- .nav-brand {
- transform-origin: left center;
- }
-
- .nav-brand-ready .nav-brand {
- visibility: hidden;
- opacity: 1;
- pointer-events: none;
- transform: none;
- }
-
- .nav-brand-ready .nav-brand.is-visible {
- visibility: visible;
- pointer-events: auto;
- }
-
- .nav-cta {
- display: inline-flex;
- min-height: 3.25rem;
- align-items: center;
- justify-content: center;
- padding: 0.75rem 1.15rem;
- background-color: #000 !important;
- color: #fff !important;
- font-size: clamp(1.05rem, 5vw, 1.3rem);
- line-height: 1;
- letter-spacing: -0.035em;
- }
-
- .nav-cta:hover,
- .nav-cta:focus-visible {
- background-color: #fff !important;
- color: #000 !important;
- }
-
- .nav-brand-word {
- display: flex;
- gap: 0.22em;
- padding-right: 0.08em;
- font-size: clamp(1.25rem, 5.8vw, 1.7rem);
- line-height: 0.9;
- letter-spacing: -0.055em;
- white-space: nowrap;
- }
-
- .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(1.5rem, 2.2vw, 2rem);
- }
-
- .nav-cta {
- min-height: 2.75rem;
- padding: 0.72rem 1.35rem;
- font-size: clamp(1.2rem, 2vw, 1.6rem);
- }
- }
-
- .hero-vignette {
- position: absolute;
- inset: 0;
- background: #000;
- pointer-events: none;
- z-index: 1;
- }
-
- @media (max-width: 767px) {
- #hero {
- --mobile-nav-height: 5.75rem;
- box-sizing: border-box;
- height: var(--mobile-hero-height, 100svh);
- min-height: var(--mobile-hero-height, 100svh);
- align-items: stretch;
- padding-top: var(--mobile-nav-height);
- padding-bottom: 0;
- }
-
- .hero-content {
- align-content: center;
- gap: 0;
- padding-block: clamp(1.25rem, 4svh, 2rem);
- }
-
- .hero-content h1 {
- font-size: clamp(2.75rem, 17vw, 4.5rem);
- font-size: clamp(2.75rem, min(17vw, 9.5svh), 4.5rem);
- }
-
- }
-
- .hero-content h1 {
- width: max-content;
- padding-right: 0.1em;
- font-size: 53vw;
- font-size: min(53vw, calc((100svh - var(--site-nav-height)) * 1.25));
- line-height: 0.8;
- letter-spacing: -0.075em;
- }
-
- .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;
- }
-
- .hero-scroll-hint {
- position: absolute;
- bottom: max(1rem, env(safe-area-inset-bottom));
- left: 50%;
- z-index: 3;
- display: inline-flex;
- align-items: center;
- gap: 0.55rem;
- color: #fff;
- font-size: clamp(0.7rem, 1.1vw, 0.85rem);
- font-weight: 500;
- line-height: 1;
- letter-spacing: 0.08em;
- text-decoration: none;
- text-transform: uppercase;
- transform: translateX(-50%);
- transition: opacity 160ms ease;
- }
-
- .hero-scroll-hint::after {
- width: 0.58em;
- height: 0.58em;
- border-right: 0.12em solid currentColor;
- border-bottom: 0.12em solid currentColor;
- content: "";
- animation: hero-scroll-nudge 1.65s cubic-bezier(0.45, 0, 0.2, 1) infinite;
- }
-
- .hero-scroll-hint:hover {
- opacity: 0.68;
- }
-
- .hero-scroll-hint:focus-visible {
- outline: 2px solid var(--neon);
- outline-offset: 0.4rem;
- }
-
- @keyframes hero-scroll-nudge {
- 0%, 100% { transform: translateY(-0.12rem) rotate(45deg); }
- 50% { transform: translateY(0.2rem) rotate(45deg); }
- }
-
- @keyframes neon-letter-flicker {
- 0%, 4.8%, 5.4%, 5.9%, 37.5%, 38%, 38.6%, 100% {
- opacity: 1;
- -webkit-text-stroke-color: rgba(235,255,120,0.58);
- filter:
- drop-shadow(0 0 0.012em rgba(235,255,120,0.54))
- drop-shadow(0 0 0.045em rgba(204,255,0,0.72))
- drop-shadow(0 0 0.105em rgba(204,255,0,0.28))
- drop-shadow(0 0 0.24em rgba(204,255,0,0.16));
- }
- 5%, 37.8% {
- opacity: 0.08;
- -webkit-text-stroke-color: rgba(204,255,0,0.18);
- filter: none;
- }
- 5.7%, 38.3% {
- opacity: 0.38;
- -webkit-text-stroke-color: rgba(204,255,0,0.36);
- filter:
- drop-shadow(0 0 0.012em rgba(204,255,0,0.34))
- drop-shadow(0 0 0.036em rgba(204,255,0,0.20));
- }
- }
-
- .work-intro {
- position: relative;
- height: 100svh;
- background: var(--page-bg);
- }
-
- .work-intro:not(.work-intro--closing) {
- height: auto;
- }
-
- .work-intro-stage {
- position: sticky;
- top: var(--site-nav-height);
- z-index: 3;
- display: flex;
- box-sizing: border-box;
- height: calc(100svh - var(--site-nav-height));
- align-items: center;
- overflow: hidden;
- padding: clamp(0.75rem, 1.5vw, 1.25rem) clamp(1.5rem, 5vw, 4rem);
- }
-
- .work-intro:not(.work-intro--closing) .work-intro-stage {
- position: relative;
- top: auto;
- height: auto;
- align-items: flex-start;
- padding-top: 0;
- padding-bottom: 25px;
- }
-
- .work-heading {
- display: flex;
- width: 100%;
- max-width: 78rem;
- flex-wrap: wrap;
- gap: 0 0.2em;
- margin: 0 auto;
- color: #fff;
- font-size: clamp(2.85rem, 8vw, 7rem);
- font-weight: 700;
- line-height: 0.94;
- letter-spacing: -0.065em;
- text-wrap: balance;
- }
-
- #work-heading {
- scroll-margin-top: calc(var(--site-nav-height) + 1rem);
- }
-
- .work-heading-words {
- display: contents;
- }
-
- .work-heading-word {
- display: inline-block;
- }
-
- .work-heading-email {
- color: inherit;
- font-weight: 500;
- text-decoration: none;
- transition: font-weight 160ms ease;
- }
-
- .work-heading-email .bi-send {
- display: inline-block;
- width: 0.9em;
- height: 0.9em;
- vertical-align: -0.06em;
- }
-
- .work-heading-email:hover,
- .work-heading-email:focus-visible {
- font-weight: 700;
- }
-
- .work-heading-email:focus-visible {
- outline: 0.055em solid #fff;
- outline-offset: 0.075em;
- }
-
- .work-intro--closing {
- height: auto;
- min-height: 0;
- }
-
- .work-intro--closing .work-intro-stage {
- position: relative;
- top: auto;
- height: auto;
- align-items: flex-start;
- padding-top: 48px;
- padding-bottom: 56px;
- }
-
- .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);
- }
-
- .work-intro.is-scroll-ready .work-heading-word.is-revealed {
- opacity: 1;
- filter: none;
- transform: none;
- }
-
- @media (prefers-reduced-motion: reduce) {
- .nav-brand-letter {
- filter: none;
- transform: none;
- will-change: auto;
- }
-
- .neon-letter,
- .neon-letter--flicker {
- animation: none;
- }
-
- .hero-letter {
- opacity: 1;
- filter: none;
- transform: none;
- will-change: auto;
- }
-
- .hero-scroll-hint::after {
- animation: none;
- }
-
- .work-intro {
- height: auto;
- }
-
- .work-intro-stage {
- position: relative;
- top: auto;
- height: auto;
- transform: none;
- will-change: auto;
- }
-
- .work-intro.is-scroll-ready .work-heading-word {
- opacity: 1;
- filter: none;
- transform: none;
- transition: none;
- }
- }
-
- .work-stage {
- box-sizing: border-box;
- padding: clamp(0rem, 0.5vw, 0.5rem) clamp(1rem, 2vw, 2rem);
- }
-
- .work-stage-content {
- width: 100%;
- max-width: none;
- min-width: 0;
- margin-inline: auto;
- }
-
- #work-scroller {
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- gap: clamp(0.3rem, 0.6vw, 0.65rem);
- overflow: visible;
- padding: 0;
- touch-action: pan-y pinch-zoom;
- }
-
- #work-scroller > article {
- position: relative;
- isolation: isolate;
- width: 100%;
- min-width: 0;
- aspect-ratio: 16 / 9;
- border-radius: 0;
- }
-
- #work-scroller > article > .portfolio-media {
- display: block;
- width: 100%;
- height: 100%;
- border-radius: 0;
- }
-
- #work-scroller > article > .portfolio-media > img {
- width: 100%;
- height: 100%;
- aspect-ratio: auto;
- object-fit: cover;
- transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
- }
-
- #work-scroller > article > .portfolio-details {
- position: absolute;
- inset: 0;
- z-index: 2;
- display: flex;
- box-sizing: border-box;
- align-items: center;
- overflow: hidden;
- padding: clamp(1rem, 2.5vw, 2.75rem);
- background: rgba(0, 0, 0, 0.94);
- opacity: 0;
- clip-path: inset(100% 0 0 0);
- pointer-events: none;
- transform: translate3d(0, 7%, 0) skewY(1.2deg);
- transform-origin: center bottom;
- transition:
- opacity 180ms ease,
- clip-path 480ms cubic-bezier(0.16, 1, 0.3, 1),
- transform 520ms cubic-bezier(0.16, 1, 0.3, 1);
- backdrop-filter: blur(0.35rem);
- }
-
- #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:focus-within > .portfolio-media > img,
- #work-scroller > article.is-description-visible > .portfolio-media > img {
- transform: scale(1.025);
- }
-
- #work-scroller > article:focus-within {
- outline: 2px solid var(--neon);
- outline-offset: -2px;
- }
-
- #work-scroller > article > .portfolio-details > p {
- width: 100%;
- margin: 0;
- color: #fff;
- font-size: clamp(1.2rem, 3vw, 3.5rem);
- font-weight: 500;
- line-height: 0.98;
- letter-spacing: -0.04em;
- text-wrap: balance;
- }
-
- .portfolio-description {
- display: block;
- color: #fff;
- }
-
- .portfolio-name {
- font-weight: 700;
- }
-
- .portfolio-live-link {
- display: inline-block;
- color: #fff;
- font-weight: 500;
- text-decoration: none;
- text-shadow: 0 0 currentColor;
- transition: text-shadow 160ms ease;
- white-space: nowrap;
- }
-
- .portfolio-live-link:hover,
- .portfolio-live-link:focus-visible {
- font-weight: 500;
- text-shadow:
- 0.018em 0 currentColor,
- -0.018em 0 currentColor;
- }
-
- .portfolio-live-link:focus-visible {
- outline: 0.09em solid #fff;
- outline-offset: 0.08em;
- }
-
- .portfolio-scroll-ready #work-scroller > article {
- opacity: var(--card-opacity, 0);
- filter: blur(var(--card-blur, 0.4rem));
- transform:
- translate3d(var(--card-x, 0), var(--card-y, 5rem), 0)
- rotate(var(--card-rotate, 0deg))
- skewY(var(--card-skew, 0deg))
- scale(var(--card-scale-x, 0.94), var(--card-scale-y, 1.04));
- transform-origin: center bottom;
- will-change: auto;
- }
-
- .portfolio-scroll-ready #work-scroller > article.is-card-animating {
- will-change: opacity, filter, transform;
- }
-
- @media (min-width: 768px) {
- #work-scroller {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
- }
-
- @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 (hover: none), (pointer: coarse) {
- #work-scroller > article {
- cursor: pointer;
- }
-
- #work-scroller > article.is-description-visible {
- cursor: default;
- }
-
- .portfolio-scroll-ready #work-scroller > article {
- filter: none;
- }
-
- #work-scroller > article > .portfolio-details {
- -webkit-backdrop-filter: none;
- backdrop-filter: none;
- }
- }
-
- @media (prefers-reduced-motion: reduce) {
- .portfolio-scroll-ready #work-scroller > article {
- opacity: 1;
- filter: none;
- transform: none;
- }
-
- #work-scroller > article > .portfolio-media > img,
- #work-scroller > article > .portfolio-details {
- transition: none;
- }
- }
-
- </style>
- <script type="application/ld+json">
- {
- "@context": "https://schema.org",
- "@graph": [
- {
- "@type": "Organization",
- "@id": "https://afterdarklabs.io/#organization",
- "name": "After Dark Labs",
- "url": "https://afterdarklabs.io/",
- "logo": "https://afterdarklabs.io/logo_assets/icon-512.png",
- "founder": {
- "@type": "Person",
- "name": "Harrison Erd",
- "url": "https://harrisonerd.com/"
- },
- "areaServed": {
- "@type": "Country",
- "name": "United States"
- },
- "description": "After Dark Labs provides South Florida website design and development for custom websites, Shopify stores, mobile apps, SaaS platforms, APIs, AI integrations, branding, hosting, and business tools."
- },
- {
- "@type": "WebSite",
- "@id": "https://afterdarklabs.io/#website",
- "url": "https://afterdarklabs.io/",
- "name": "After Dark Labs",
- "alternateName": "After Dark Labs Florida",
- "publisher": {
- "@id": "https://afterdarklabs.io/#organization"
- },
- "description": "After Dark Labs provides website design and development for custom websites, Shopify stores, mobile apps, and business tools."
- },
- {
- "@type": "Service",
- "@id": "https://afterdarklabs.io/#digital-services",
- "name": "Website, ecommerce, app, and business software development",
- "description": "Custom websites, ecommerce stores, mobile apps, SaaS platforms, branding, hosting, APIs, AI chatbots, and intelligent automation.",
- "provider": {
- "@id": "https://afterdarklabs.io/#organization"
- },
- "areaServed": {
- "@type": "Country",
- "name": "United States"
- },
- "serviceType": [
- "Custom website design and development",
- "Shopify and ecommerce development",
- "Mobile app development",
- "Custom platforms, dashboards, and API integrations",
- "Brand identity and digital marketing",
- "Hosting and business technology",
- "AI integrations, chatbots, and intelligent automation"
- ],
- "url": "https://afterdarklabs.io/"
- }
- ]
- }
- </script>
-</head>
-<body class="bg-black text-white overflow-x-hidden">
- <nav class="fixed top-0 z-50 w-full bg-black/90 backdrop-blur-lg">
- <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="After Dark Labs — go to top">
- <span class="nav-brand-word logo-font font-semibold" aria-hidden="true"><span class="nav-brand-letter">After</span><span class="nav-brand-letter">Dark</span><span class="nav-brand-letter">Labs</span></span>
- </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="After Dark Labs — go to top">
- <div>
- <span class="nav-brand-word logo-font font-semibold" aria-hidden="true"><span class="nav-brand-letter">After</span><span class="nav-brand-letter">Dark</span><span class="nav-brand-letter">Labs</span></span>
- </div>
- </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>
-
- <main>
- <section id="hero" class="hero-bg min-h-screen flex items-center pt-20" style="position:relative; overflow:hidden;">
- <div class="hero-vignette"></div>
-
- <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" aria-label="ADL"><span class="hero-letter" aria-hidden="true">A</span><span class="hero-letter" aria-hidden="true">D</span><span class="hero-letter" aria-hidden="true">L</span></h1>
- </div>
- </div>
-
- <a class="hero-scroll-hint" href="#work-heading">Scroll</a>
-
- </section>
-
- <section class="work-intro" aria-labelledby="work-heading">
- <div class="work-intro-stage">
- <h2 id="work-heading" class="work-heading" aria-label="We build custom websites and apps. Take a look at some of our work.">
- <span class="work-heading-words" aria-hidden="true">
- <span class="work-heading-word">We</span>
- <span class="work-heading-word">build</span>
- <span class="work-heading-word">custom</span>
- <span class="work-heading-word">websites</span>
- <span class="work-heading-word">and</span>
- <span class="work-heading-word">apps.</span>
- <span class="work-heading-word">Take</span>
- <span class="work-heading-word">a</span>
- <span class="work-heading-word">look</span>
- <span class="work-heading-word">at</span>
- <span class="work-heading-word">some</span>
- <span class="work-heading-word">of</span>
- <span class="work-heading-word">our</span>
- <span class="work-heading-word">work.</span>
- </span>
- </h2>
- </div>
- </section>
-
- <section id="work" class="scroll-mt-24 border-t border-white/10 bg-black" aria-label="Featured work">
- <div class="work-stage">
- <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]">
- <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">
- </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. <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]">
- <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">
- </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. <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]">
- <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">
- </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%. <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]">
- <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">
- </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. <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]">
- <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">
- </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. <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]">
- <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">
- </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. <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]">
- <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">
- </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. <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]">
- <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">
- </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. <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">
- <a class="work-heading-word work-heading-email" href="mailto:[email protected]">
- <span class="work-heading-word">Let’s</span>
- <span class="work-heading-word">move</span>
- <span class="work-heading-word">your</span>
- <span class="work-heading-word">business</span>
- <span class="work-heading-word">forward</span>
- <span class="work-heading-word">
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-send" viewBox="0 0 16 16">
- <path d="M15.854.146a.5.5 0 0 1 .11.54l-5.819 14.547a.75.75 0 0 1-1.329.124l-3.178-4.995L.643 7.184a.75.75 0 0 1 .124-1.33L15.314.037a.5.5 0 0 1 .54.11ZM6.636 10.07l2.761 4.338L14.13 2.576zm6.787-8.201L1.591 6.602l4.339 2.76z"/>
- </svg>
- </span>
- </a>
- </span>
- </h2>
- </div>
- </div>
- </section>
-
- </main>
-
- <script>
- (function () {
- const hero = document.getElementById('hero');
- if (!hero) return;
-
- const nav = document.querySelector('nav');
- const mobileLayout = window.matchMedia('(max-width: 767px)');
- let lockedWidth = 0;
- let resizeFrame = 0;
-
- function lockMobileHeroHeight() {
- resizeFrame = 0;
-
- if (!mobileLayout.matches) {
- hero.style.removeProperty('--mobile-hero-height');
- hero.style.removeProperty('--mobile-nav-height');
- lockedWidth = 0;
- return;
- }
-
- const navHeight = Math.round(nav?.getBoundingClientRect().height || 0);
- if (navHeight) hero.style.setProperty('--mobile-nav-height', `${navHeight}px`);
-
- const layoutWidth = Math.round(document.documentElement.clientWidth);
- if (Math.abs(layoutWidth - lockedWidth) < 2) return;
-
- const visibleHeight = Math.round(window.visualViewport?.height || window.innerHeight);
- if (!visibleHeight) return;
-
- lockedWidth = layoutWidth;
- hero.style.setProperty('--mobile-hero-height', `${visibleHeight}px`);
- }
-
- function scheduleHeroHeightLock() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(lockMobileHeroHeight);
- }
-
- window.addEventListener('resize', scheduleHeroHeightLock);
- window.visualViewport?.addEventListener('resize', scheduleHeroHeightLock);
- mobileLayout.addEventListener?.('change', scheduleHeroHeightLock);
- lockMobileHeroHeight();
- })();
-
- (function () {
- const hero = document.getElementById('hero');
- const heading = hero?.querySelector('.hero-content h1');
- const letters = Array.from(heading?.querySelectorAll('.hero-letter') || []);
- const scrollHint = hero?.querySelector('.hero-scroll-hint');
- const nav = document.querySelector('nav');
- const brands = Array.from(document.querySelectorAll('.nav-brand'));
- 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 resizeFrame = 0;
-
- function clamp(value, minimum = 0, maximum = 1) {
- return Math.min(maximum, Math.max(minimum, value));
- }
-
- function updateHeroLetters() {
- updateFrame = 0;
-
- 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));
- if (scrollHint) {
- scrollHint.style.opacity = revealBrand ? '0' : '1';
- scrollHint.style.pointerEvents = revealBrand ? 'none' : 'auto';
- scrollHint.toggleAttribute('inert', 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));
-
- if (scrollHint && index === letters.length - 1) {
- const hintVisible = remaining > 0.02;
- scrollHint.style.opacity = remaining.toFixed(4);
- scrollHint.style.pointerEvents = hintVisible ? 'auto' : 'none';
- scrollHint.toggleAttribute('inert', !hintVisible);
- }
-
- 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 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');
- measureHeroLetters();
- })();
-
- (function () {
- const intro = document.querySelector('.work-intro:not(.work-intro--closing)');
- const heading = intro?.querySelector('.work-heading');
- const firstCard = document.querySelector('#work-scroller > article');
- const words = Array.from(intro?.querySelectorAll('.work-heading-word') || []);
- if (!intro || !heading || !words.length) return;
-
- const nav = document.querySelector('nav');
- 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 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) {
- words.forEach(word => word.classList.add('is-revealed'));
- revealedWords = words.length;
- return;
- }
-
- const progress = window.scrollY <= 1
- ? 0
- : 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 measureWorkIntro() {
- resizeFrame = 0;
- intro.classList.toggle('is-scroll-ready', !reduceMotion.matches);
- intro.classList.remove('is-motion-ready');
-
- const navHeight = nav?.getBoundingClientRect().height || 0;
- const viewportHeight = window.visualViewport?.height || window.innerHeight;
- const headingCenter = getDocumentTop(heading) + heading.offsetHeight / 2;
- const visualCenter = navHeight + (viewportHeight - navHeight) * 0.58;
- const cardRevealStart = firstCard
- ? getDocumentTop(firstCard) - viewportHeight * 0.94
- : headingCenter - visualCenter + viewportHeight * 0.32;
- revealStart = headingCenter - visualCenter;
- const revealEnd = Math.max(
- cardRevealStart + viewportHeight * 0.06,
- revealStart + viewportHeight * 0.24
- );
- revealDistance = Math.max(1, revealEnd - revealStart);
- revealedWords = -1;
- updateWorkIntro();
-
- if (!reduceMotion.matches) {
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => intro.classList.add('is-motion-ready'));
- });
- }
- }
-
- function scheduleWorkIntroUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateWorkIntro);
- }
-
- function scheduleWorkIntroMeasure() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureWorkIntro);
- }
-
- window.addEventListener('scroll', scheduleWorkIntroUpdate, { passive: true });
- window.addEventListener('resize', scheduleWorkIntroMeasure);
- window.addEventListener('pageshow', scheduleWorkIntroMeasure);
- reduceMotion.addEventListener('change', scheduleWorkIntroMeasure);
- document.fonts?.ready.then(scheduleWorkIntroMeasure);
- measureWorkIntro();
- })();
-
- (function () {
- const section = document.getElementById('work');
- const cards = Array.from(section?.querySelectorAll('#work-scroller > article') || []);
- if (!cards.length) return;
-
- const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
- const touchDisclosure = window.matchMedia('(hover: none), (pointer: coarse)');
- let cardTops = [];
- let cardStates = cards.map(() => -1);
- let layoutWidth = 0;
- let layoutHeight = 0;
- let lastScrollY = -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 fitDescriptions() {
- cards.forEach(card => {
- const panel = card.querySelector(':scope > .portfolio-details');
- const copy = panel?.querySelector('p');
- if (!panel || !copy) return;
-
- const panelStyle = window.getComputedStyle(panel);
- const horizontalPadding = parseFloat(panelStyle.paddingLeft) + parseFloat(panelStyle.paddingRight);
- const verticalPadding = parseFloat(panelStyle.paddingTop) + parseFloat(panelStyle.paddingBottom);
- const availableWidth = Math.max(1, panel.clientWidth - horizontalPadding);
- const availableHeight = Math.max(1, panel.clientHeight - verticalPadding);
- let minimum = 12;
- let maximum = Math.min(88, Math.max(34, card.clientWidth / 7.2));
-
- for (let iteration = 0; iteration < 10; iteration += 1) {
- const size = (minimum + maximum) / 2;
- copy.style.fontSize = `${size}px`;
-
- if (copy.scrollHeight <= availableHeight + 1 && copy.scrollWidth <= availableWidth + 1) {
- minimum = size;
- } else {
- maximum = size;
- }
- }
-
- copy.style.fontSize = `${Math.floor(minimum * 10) / 10}px`;
- });
- }
-
- function updateCards() {
- updateFrame = 0;
- if (reduceMotion.matches) {
- cards.forEach(card => card.classList.remove('is-card-animating'));
- return;
- }
-
- const scrollY = Math.max(0, window.scrollY);
- const viewportHeight = layoutHeight || document.documentElement.clientHeight || window.innerHeight;
- if (scrollY === lastScrollY) return;
-
- lastScrollY = scrollY;
- const revealDistance = Math.max(240, viewportHeight * 0.58);
- const travelX = Math.min(72, document.documentElement.clientWidth * 0.06);
-
- cards.forEach((card, index) => {
- const progress = clamp((scrollY + viewportHeight * 0.94 - cardTops[index]) / revealDistance);
- const state = progress <= 0 ? 0 : progress >= 1 ? 1 : 2;
- if (cardStates[index] !== state) {
- card.classList.toggle('is-card-animating', state === 2);
- }
- if (state !== 2 && cardStates[index] === state) return;
-
- cardStates[index] = state;
- const eased = 1 - Math.pow(1 - progress, 3);
- const remaining = 1 - eased;
- const direction = index % 2 === 0 ? -1 : 1;
- const wave = Math.sin(progress * Math.PI * 3) * remaining;
- const x = direction * (remaining * travelX + wave * 20);
- const y = remaining * 96 - Math.sin(progress * Math.PI) * 10;
- const rotation = direction * (remaining * 5 + wave * 2.4);
- const skew = direction * wave * 1.6;
- const scaleX = 1 - remaining * 0.07 + Math.abs(wave) * 0.025;
- const scaleY = 1 + remaining * 0.065 - wave * 0.02;
- const opacity = clamp(progress * 1.65);
- const blur = remaining * 7;
-
- card.style.setProperty('--card-x', `${x.toFixed(2)}px`);
- card.style.setProperty('--card-y', `${y.toFixed(2)}px`);
- card.style.setProperty('--card-rotate', `${rotation.toFixed(2)}deg`);
- card.style.setProperty('--card-skew', `${skew.toFixed(2)}deg`);
- card.style.setProperty('--card-scale-x', scaleX.toFixed(4));
- card.style.setProperty('--card-scale-y', scaleY.toFixed(4));
- card.style.setProperty('--card-opacity', opacity.toFixed(4));
- card.style.setProperty('--card-blur', `${blur.toFixed(2)}px`);
- });
- }
-
- function measureCards() {
- resizeFrame = 0;
- const nextLayoutWidth = document.documentElement.clientWidth;
- const nextLayoutHeight = document.documentElement.clientHeight || window.innerHeight;
- const widthChanged = Math.abs(nextLayoutWidth - layoutWidth) >= 2;
- const desktopHeightChanged = !touchDisclosure.matches && Math.abs(nextLayoutHeight - layoutHeight) >= 2;
- if (layoutWidth && !widthChanged && !desktopHeightChanged) {
- scheduleCardUpdate();
- return;
- }
-
- layoutWidth = nextLayoutWidth;
- layoutHeight = nextLayoutHeight;
- cardTops = cards.map(getDocumentTop);
- cardStates = cards.map(() => -1);
- lastScrollY = -1;
- fitDescriptions();
- updateCards();
- }
-
- function scheduleCardUpdate() {
- if (!updateFrame) updateFrame = window.requestAnimationFrame(updateCards);
- }
-
- function scheduleCardMeasure() {
- if (!resizeFrame) resizeFrame = window.requestAnimationFrame(measureCards);
- }
-
- function scheduleCardRemeasure() {
- layoutWidth = 0;
- scheduleCardMeasure();
- }
-
- function closeDescriptions(exceptCard = null) {
- cards.forEach(card => {
- if (card !== exceptCard) card.classList.remove('is-description-visible');
- });
- }
-
- cards.forEach(card => {
- card.addEventListener('click', event => {
- if (!touchDisclosure.matches || event.target.closest('.portfolio-live-link')) return;
-
- closeDescriptions(card);
- card.classList.add('is-description-visible');
- });
- });
-
- document.addEventListener('pointerdown', event => {
- if (!touchDisclosure.matches || event.target.closest('#work-scroller > article')) return;
- closeDescriptions();
- const activeCard = document.activeElement?.closest?.('#work-scroller > article');
- if (activeCard) document.activeElement.blur();
- }, { passive: true });
-
- touchDisclosure.addEventListener('change', () => closeDescriptions());
-
- section.classList.add('portfolio-scroll-ready');
- window.addEventListener('scroll', scheduleCardUpdate, { passive: true });
- window.addEventListener('resize', scheduleCardMeasure);
- window.visualViewport?.addEventListener('resize', scheduleCardMeasure);
- window.addEventListener('pageshow', scheduleCardRemeasure);
- reduceMotion.addEventListener('change', scheduleCardRemeasure);
- document.fonts?.ready.then(scheduleCardRemeasure);
- 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>