This is the code for this website.
View HTML Code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Joe Wolff | Resume</title>
<meta name="description" content="Joe Wolff interactive resume and portfolio." />
<style>
:root {
--red: #ff3b3b;
--blue: #2e7cff;
--navy: #11376c;
--gold: #ffbf2f;
--white: #ffffff;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
background:
radial-gradient(circle at 15% 10%, rgba(46,124,255,.18), transparent 22%),
radial-gradient(circle at 85% 14%, rgba(255,59,59,.12), transparent 18%),
linear-gradient(180deg, #fff7d6 0%, #ffeeb0 52%, #fff4cc 100%);
color: var(--navy);
overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
.bg {
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
background:
radial-gradient(circle at 10% 20%, rgba(46,124,255,.14) 0 2px, transparent 2.6px),
radial-gradient(circle at 20% 72%, rgba(255,59,59,.14) 0 2px, transparent 2.6px),
radial-gradient(circle at 62% 18%, rgba(46,124,255,.14) 0 2px, transparent 2.6px),
radial-gradient(circle at 84% 24%, rgba(255,191,47,.18) 0 2px, transparent 2.8px),
radial-gradient(circle at 76% 64%, rgba(46,124,255,.14) 0 2px, transparent 2.6px),
radial-gradient(circle at 34% 88%, rgba(255,59,59,.10) 0 2px, transparent 2.6px);
opacity: .9;
}
.wrap {
width: min(760px, calc(100% - 22px));
margin: 0 auto;
padding: 18px 0 42px;
}
@keyframes float {
0%,100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
@keyframes sparkle {
0%,100% { transform: scale(1); opacity: .8; }
50% { transform: scale(1.18); opacity: 1; }
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes shine {
from { transform: translateX(-120%); }
to { transform: translateX(120%); }
}
@keyframes shimmer {
0% { background-position: 0% center; }
100% { background-position: 200% center; }
}
@keyframes firePulse {
0%,100% { transform: scale(.82); opacity: .28; }
50% { transform: scale(1.06); opacity: .72; }
}
@keyframes mascotBob {
0%,100% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-4px) rotate(-1deg); }
}
50% { transform: scale(1.06); opacity: .72; }
}
@keyframes finalGlow {
0%,100% { box-shadow: 0 14px 28px rgba(255,191,47,.14), 0 0 0 rgba(255,191,47,0); }
50% { box-shadow: 0 18px 34px rgba(255,191,47,.26), 0 0 24px rgba(255,191,47,.24); }
}
@keyframes logoGlow {
from {
filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.25));
}
to {
filter: drop-shadow(0 0 18px rgba(255, 215, 0, 0.6));
}
}
50% { box-shadow: 0 18px 34px rgba(255,191,47,.26), 0 0 24px rgba(255,191,47,.24); }
}
@keyframes caPulse {
0%,100% {
box-shadow: 0 0 0 0 rgba(255,59,59,.28), 0 14px 30px rgba(37,99,235,.16);
}
50% {
box-shadow: 0 0 20px 6px rgba(255,255,255,.35), 0 18px 36px rgba(37,99,235,.22);
}
}
@keyframes clickFlash {
0% { transform: scale(1); filter: brightness(1); }
35% { transform: scale(1.07); filter: brightness(1.12); }
100% { transform: scale(1); filter: brightness(1); }
}
.fx-layer {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 9999;
}
50% { box-shadow: 0 18px 34px rgba(255,191,47,.26), 0 0 24px rgba(255,191,47,.24); }
}
.hero {
text-align: center;
animation: fadeUp .6s ease both;
}
.mini-logo {
width: 210px;
height: 210px;
margin: 8px auto 10px;
border-radius: 50%;
background: linear-gradient(135deg, #ffbf2f, #ffffff, #2e7cff);
border: 4px solid rgba(255,191,47,.95);
box-shadow: 0 18px 34px rgba(17,55,108,.18), 0 0 22px rgba(255,191,47,.28);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
animation: float 4s ease-in-out infinite;
}
.mini-logo img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 22%;
display: block;
background: none;
transform: none;
filter: none;
animation: none;
}
.hero h1 {
font-size: clamp(2.8rem, 11vw, 5.4rem);
line-height: .9;
letter-spacing: .04em;
text-align: center;
font-weight: 900;
text-transform: uppercase;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
background: linear-gradient(90deg, #ff3b3b 0%, #ffffff 45%, #2e7cff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 200% auto;
animation: shimmer 5s linear infinite;
-webkit-text-stroke: 1px rgba(17,55,108,0.22);
text-shadow: 0 3px 8px rgba(0,0,0,.12);
}
.hero-subtitle {
margin-top: 6px;
font-weight: 900;
color: #11376c;
text-align: center;
letter-spacing: .02em;
text-transform: uppercase;
}
.hero-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 14px;
flex-wrap: wrap;
position: relative;
z-index: 3;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 14px 18px;
border-radius: 20px;
border: none;
font-weight: 900;
transition: .2s ease;
box-shadow: 0 10px 24px rgba(18,48,95,.12);
color: white;
}
.btn:nth-child(1) { background: linear-gradient(135deg,#ff3b3b,#ff6b6b); }
.btn:nth-child(2) { background: linear-gradient(135deg,#ffffff,#dbeafe); color:#11376c; }
.btn:nth-child(3) { background: linear-gradient(135deg,#2e7cff,#60a5fa); }
.btn:hover { transform: translateY(-2px) scale(1.03); box-shadow: 0 14px 30px rgba(18,48,95,.18); }
.copy-btn {
margin-bottom: 4px;
position: relative;
overflow: hidden;
width: 100%;
padding: 18px 28px;
border-radius: 999px;
border: none;
background: linear-gradient(90deg, #ff3b3b 0%, #ffffff 45%, #2e7cff 100%);
color: #0b1b3b;
font-weight: 900;
font-size: 18px;
letter-spacing: .05em;
text-transform: uppercase;
box-shadow: 0 14px 30px rgba(37,99,235,.16);
cursor: pointer;
transition: transform .18s ease, box-shadow .18s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
animation: caPulse 2.2s ease-in-out infinite;
}
.copy-btn:hover {
transform: translateY(-3px) scale(1.04);
box-shadow: 0 18px 36px rgba(37,99,235,.24);
}
.copy-btn::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(110deg, transparent 20%, rgba(255,255,255,.65) 50%, transparent 80%);
transform: translateX(-120%);
animation: shine 3.8s linear infinite;
}
.copy-btn span,
.copy-btn img { position: relative; z-index: 1; }
.copy-logo {
width: 34px;
height: 34px;
border-radius: 50%;
object-fit: contain;
background: none;
box-shadow: none;
flex: 0 0 34px;
}
.card {
position: relative;
overflow: hidden;
border-radius: 30px;
background: rgba(255,255,255,.9);
border: 2px solid rgba(17,55,108,.06);
box-shadow: 0 20px 44px rgba(17,55,108,.08);
padding: 20px;
margin-top: 16px;
backdrop-filter: blur(10px);
}
.card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 6px;
background: linear-gradient(90deg, var(--red), var(--white), var(--blue));
}
.logo-bubble {
max-width: 260px;
margin: 16px auto 0;
padding: 10px;
background: rgba(255,255,255,.65);
box-shadow: 0 14px 28px rgba(17,55,108,.06);
}
.logo-bubble::before {
display: none;
}
.bubble-logo {
width: 100%;
max-width: 135px;
display: block;
margin: 0 auto;
filter: drop-shadow(0 10px 16px rgba(17,55,108,.10));
}
.mascot-slot {
display: flex;
justify-content: center;
align-items: center;
margin: 10px auto 8px;
padding: 0;
background: transparent;
}
.mascot-wrap {
position: relative;
width: 100%;
max-width: 160px;
animation: mascotBob 3.2s ease-in-out infinite;
transform-origin: bottom center;
background: transparent;
box-shadow: none;
border: none;
isolation: isolate;
}
.mascot-img {
width: 100%;
display: block;
margin: 0 auto;
background: transparent;
mix-blend-mode: normal;
filter: drop-shadow(0 10px 18px rgba(17,55,108,.14));
}
.mini-fuse-spark {
position: absolute;
left: 54%;
top: 59%;
width: 14px;
height: 14px;
border-radius: 50%;
background: radial-gradient(circle, #fff7b8 0%, #ffbf2f 35%, #ff7a00 65%, rgba(255,122,0,0) 100%);
box-shadow: 0 0 8px rgba(255,191,47,.8), 0 0 16px rgba(255,122,0,.55), 0 0 24px rgba(255,80,0,.35);
animation: fusePulse 1s ease-in-out infinite;
pointer-events: none;
}
.mini-ember {
position: absolute;
left: 55%;
top: 60%;
width: 5px;
height: 5px;
border-radius: 50%;
background: radial-gradient(circle, #fff 0%, #ffbf2f 45%, #ff6b00 100%);
box-shadow: 0 0 6px rgba(255,140,0,.6);
opacity: 0;
pointer-events: none;
animation: emberFloat 1.2s linear infinite;
}
.mini-ember.m1 { --dx: -16px; --dy: -12px; animation-delay: 0s; }
.mini-ember.m2 { --dx: 8px; --dy: -18px; animation-delay: .2s; }
.mini-ember.m3 { --dx: 16px; --dy: -10px; animation-delay: .4s; }
.mini-ember.m4 { --dx: -8px; --dy: 10px; animation-delay: .1s; }
.eagle-zone {
position: relative;
min-height: 360px;
display: grid;
place-items: center;
background: linear-gradient(180deg, #ffffff, #f6faff);
overflow: hidden;
}
.star {
position: absolute;
font-size: 1.2rem;
animation: sparkle 2.8s ease-in-out infinite;
user-select: none;
}
.s1 { top: 18px; left: 24px; color: var(--blue); }
.s2 { top: 36px; right: 28px; color: var(--red); animation-delay: -.8s; }
.s3 { top: 110px; left: 8%; color: var(--gold); animation-delay: -1.4s; }
.s4 { top: 130px; right: 8%; color: var(--blue); animation-delay: -.4s; }
.s5 { bottom: 62px; left: 12%; color: var(--red); animation-delay: -1.1s; }
.s6 { bottom: 44px; right: 12%; color: var(--gold); animation-delay: -.2s; }
.halo {
position: absolute;
width: 320px;
height: 320px;
border-radius: 50%;
background: radial-gradient(circle, rgba(46,124,255,.16), rgba(255,255,255,0) 68%);
animation: haloPulse 3.4s ease-in-out infinite;
}
@keyframes haloPulse {
0%,100% { transform: scale(.96); opacity: .7; }
50% { transform: scale(1.06); opacity: 1; }
}
.fireworks {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.burst {
position: absolute;
width: 130px;
height: 130px;
border-radius: 50%;
opacity: .9;
animation: firePulse 2.8s ease-in-out infinite;
}
.burst::before,
.burst::after {
content: "";
position: absolute;
inset: 0;
border-radius: 50%;
background: repeating-conic-gradient(from 0deg, currentColor 0deg 10deg, transparent 10deg 20deg);
-webkit-mask: radial-gradient(circle, transparent 0 34%, #000 35% 58%, transparent 59% 100%);
mask: radial-gradient(circle, transparent 0 34%, #000 35% 58%, transparent 59% 100%);
}
.burst::after {
transform: scale(.62);
opacity: .7;
}
.b1 { top: 28px; left: 40px; color: rgba(46,124,255,.55); animation-delay: -.6s; }
.glasses-glow {
position: absolute;
top: 88px;
left: 50%;
width: 135px;
height: 60px;
transform: translateX(-50%);
border-radius: 999px;
background: linear-gradient(90deg, rgba(46,124,255,.18), rgba(255,255,255,.12), rgba(255,59,59,.18));
filter: blur(14px);
opacity: .65;
animation: glassesPulse 2.8s ease-in-out infinite;
z-index: 1;
pointer-events: none;
}
@keyframes glassesPulse {
0%,100% { opacity: .4; transform: translateX(-50%) scale(.96); }
50% { opacity: .9; transform: translateX(-50%) scale(1.06); }
}
.b2 { top: 38px; right: 42px; color: rgba(255,59,59,.52); animation-delay: -1.4s; }
.b3 { bottom: 58px; left: 56px; color: rgba(255,191,47,.55); animation-delay: -.2s; }
.b4 { bottom: 52px; right: 54px; color: rgba(46,124,255,.42); animation-delay: -1.8s; }
.eagle-main {
position: relative;
z-index: 2;
width: 125%;
max-width: 560px;
display: block;
object-fit: cover;
transform: translateY(12px) scale(1.1);
filter: drop-shadow(0 22px 32px rgba(17,55,108,.22));
animation: float 4.2s ease-in-out infinite, logoPop 5s ease-in-out infinite;
pointer-events: none;
}
@keyframes logoPop {
0%,100% { filter: drop-shadow(0 22px 32px rgba(17,55,108,.22)); }
50% { filter: drop-shadow(0 24px 38px rgba(255,191,47,.28)) drop-shadow(0 0 18px rgba(46,124,255,.18)); }
}
.section-title {
text-align: center;
font-size: 1.35rem;
font-weight: 900;
color: var(--navy);
margin-bottom: 6px;
letter-spacing: .02em;
text-transform: uppercase;
}
.section-sub {
text-align: center;
color: rgba(17,55,108,.72);
font-size: .96rem;
line-height: 1.5;
margin-bottom: 16px;
font-weight: 900;
letter-spacing: .01em;
}
.countdown {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.time-box {
border-radius: 22px;
background: linear-gradient(180deg, #ffffff, #f3f8ff);
border: 1px solid rgba(17,55,108,.08);
text-align: center;
padding: 16px 8px;
position: relative;
overflow: hidden;
}
.time-box::after {
content: none;
}
.time-value {
position: relative;
z-index: 1;
font-size: clamp(2rem, 8vw, 3.5rem);
font-weight: 900;
line-height: 1;
letter-spacing: -.06em;
color: var(--navy);
}
.time-label {
position: relative;
z-index: 1;
margin-top: 7px;
font-size: .72rem;
letter-spacing: .16em;
text-transform: uppercase;
font-weight: 900;
color: rgba(17,55,108,.58);
}
.count-note {
margin-top: 12px;
text-align: center;
color: rgba(17,55,108,.72);
font-weight: 700;
font-size: .95rem;
}
.roadmap {
display: grid;
gap: 14px;
width: 100%;
max-width: 760px;
margin: 0 auto;
}
.step {
min-height: 92px;
padding: 20px 22px;
border-radius: 24px;
background: linear-gradient(180deg, #ffffff, #f5f9ff);
border: 1px solid rgba(17,55,108,.08);
box-shadow: 0 10px 22px rgba(17,55,108,.06);
transition: transform .2s ease, box-shadow .2s ease;
}
.step:hover {
transform: translateY(-2px);
box-shadow: 0 16px 28px rgba(17,55,108,.1);
}
.step h3 {
margin: 0;
font-size: 1.05rem;
font-weight: 900;
color: var(--navy);
letter-spacing: -.01em;
text-align: center;
}
.step p {
margin: 8px 0 0;
font-size: 1.15rem;
line-height: 1.5;
color: #000000;
text-align: center;
font-weight: 900;
letter-spacing: .01em;
text-transform: uppercase;
}
.step.step:nth-of-type(2) {
background: linear-gradient(180deg, #ffffff, #fff9dc);
border: 2px solid rgba(255,235,170,.4);
box-shadow: 0 8px 18px rgba(255,235,170,.08);
}
.final-step {
background: linear-gradient(180deg, #fff7d6, #ffe27a);
border: 2px solid rgba(255,191,47,.6);
box-shadow: 0 14px 28px rgba(255,191,47,.18);
animation: finalGlow 2.4s ease-in-out infinite;
}
.final-step:last-of-type {
background: linear-gradient(180deg, #ffe27a, #ffc400);
border: 2px solid rgba(255,170,0,.8);
box-shadow: 0 16px 32px rgba(255,170,0,.28);
}
.final-step:last-of-type {
background: linear-gradient(180deg, #ffe27a, #ffc400);
border: 2px solid rgba(255,170,0,.8);
box-shadow: 0 16px 32px rgba(255,170,0,.28);
}
.flag-box {
width: 100%;
aspect-ratio: 2048 / 550;
min-height: 0;
height: auto;
border-radius: 34px;
overflow: hidden;
position: relative;
background:
radial-gradient(circle at 50% 45%, rgba(255,255,255,.92), rgba(255,255,255,.62) 62%, rgba(240,246,255,.72) 100%);
border: 2px solid rgba(255,255,255,.78);
box-shadow:
0 22px 44px rgba(17,55,108,.16),
inset 0 0 24px rgba(255,255,255,.46),
0 0 0 2px rgba(255,191,47,.14);
display: flex;
align-items: center;
justify-content: center;
}
.flag-inner {
width: 100%;
height: 100%;
border-radius: 28px;
overflow: hidden;
background: transparent;
position: relative;
}
.flag-image {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center center;
display: block;
}
.flag-rim{
position:absolute;
inset:0;
border-radius:28px;
border:2px solid rgba(255,191,47,.58);
box-shadow:
inset 0 0 12px rgba(255,191,47,.18),
inset 0 0 28px rgba(255,255,255,.10),
0 0 18px rgba(255,191,47,.10);
z-index:3;
pointer-events:none;
}
.flag-caption{
margin-top: 10px;
text-align:center;
color: rgba(17,55,108,.68);
font-size: .82rem;
line-height: 1.4;
font-weight: 900;
letter-spacing: .04em;
text-transform: uppercase;
}
.ca {
display: grid;
gap: 10px;
text-align: center;
}
.ca code {
display: block;
padding: 14px 16px;
border-radius: 20px;
background: #fff;
border: 1px dashed rgba(17,55,108,.14);
color: var(--navy);
font-size: .95rem;
font-weight: 900;
overflow-wrap: anywhere;
letter-spacing: .02em;
}
.small {
color: rgba(17,55,108,.68);
font-size: .92rem;
line-height: 1.5;
font-weight: 900;
letter-spacing: .01em;
text-transform: uppercase;
}
@media (max-width: 640px) {
.wrap { width: calc(100% - 16px); }
.card { padding: 16px; border-radius: 24px; }
.eagle-zone { min-height: 300px; }
.countdown { grid-template-columns: repeat(2, 1fr); }
.hero h1 { font-size: clamp(2.4rem, 14vw, 4rem); }
.logo-bubble {
max-width: 200px;
}
.bubble-logo {
max-width: 140px;
}
}
.hero-years {
margin-bottom: 4px !important;
}
.hero h1 {
margin-top: -8px;
margin-bottom: 2px;
}
.hero-subtitle {
margin-top: 2px;
}
.hero-buttons {
margin-top: 10px;
}
code#email {
display: block;
margin-top: 4px;
padding: 8px 16px;
font-size: 20px;
font-weight: 700;
color: #11376c;
text-align: center;
border: none;
background: transparent;
letter-spacing: 0.05em;
font-family: inherit;
transform: translateY(-6px);
}
.resume-copy {
text-align: left;
font-family: 'Trebuchet MS', Arial, sans-serif;
color: #11376c;
font-size: 1rem;
line-height: 1.75;
font-weight: 800;
}
.resume-copy p {
margin: 0 0 14px;
}
.resume-copy p:last-child {
margin-bottom: 0;
}
.hero-years {
font-size: clamp(0.9rem, 2.5vw, 1.1rem) !important;
letter-spacing: 0.03em;
line-height: 1.35;
max-width: 100%;
padding: 0 8px;
white-space: normal;
}
/* Open hero layout - no large hero bubble */
.wrap {
width: min(760px, calc(100% - 22px));
margin: 0 auto;
padding: 18px 0 42px;
}
.hero {
text-align: center;
animation: fadeUp .6s ease both;
padding: 0;
margin: 0 0 16px;
background: transparent !important;
border: none !important;
box-shadow: none !important;
border-radius: 0 !important;
overflow: visible !important;
}
.hero::before { display: none !important; }
.hero > * { position: relative; z-index: 1; }
.hero-years {
font-weight: 900;
font-size: clamp(.95rem, 2.5vw, 1.1rem) !important;
margin-bottom: 4px !important;
color: #11376c !important;
text-align: center;
width: 100%;
display: block;
letter-spacing: .03em !important;
text-transform: none !important;
line-height: 1.35;
padding: 0 8px;
white-space: normal;
}
.mini-logo {
width: 205px !important;
height: 205px !important;
margin: 8px auto 8px !important;
border-radius: 50% !important;
background: linear-gradient(135deg, #ffbf2f, #ffffff 48%, #2e7cff) !important;
border: 4px solid rgba(255,191,47,.95) !important;
box-shadow: 0 18px 34px rgba(17,55,108,.18), 0 0 22px rgba(255,191,47,.28) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
overflow: hidden !important;
animation: float 4s ease-in-out infinite;
padding: 0 !important;
}
.mini-logo img {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
object-position: center 24% !important;
display: block !important;
border-radius: 50% !important;
background: none !important;
transform: none !important;
filter: none !important;
animation: none !important;
}
.hero h1 {
font-size: clamp(2.8rem, 11vw, 5.4rem) !important;
line-height: .9;
letter-spacing: .04em !important;
text-align: center;
font-weight: 900;
text-transform: uppercase;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
margin-top: -4px !important;
margin-bottom: 2px !important;
background: linear-gradient(90deg, #ff3b3b 0%, #ffffff 45%, #2e7cff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-size: 200% auto;
animation: shimmer 5s linear infinite;
-webkit-text-stroke: 1px rgba(17,55,108,0.22) !important;
text-shadow: 0 3px 8px rgba(0,0,0,.12) !important;
}
.hero-subtitle {
margin-top: 2px !important;
font-weight: 900;
color: #11376c !important;
text-align: center;
letter-spacing: .02em;
text-transform: uppercase;
font-size: clamp(.95rem, 3vw, 1.25rem);
}
.hero-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 10px !important;
flex-wrap: wrap;
position: relative;
z-index: 3;
}
.btn {
min-width: 120px;
border-radius: 20px !important;
}
@media (max-width: 640px) {
.wrap { width: calc(100% - 16px); }
.hero { margin-bottom: 16px; }
.mini-logo { width: 180px !important; height: 180px !important; }
.hero h1 { font-size: clamp(2.4rem, 14vw, 4rem) !important; }
.btn { min-width: 96px; padding: 12px 14px; }
}
/* Resume Popup Modal */
.resume-modal {
position: fixed;
inset: 0;
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
padding: 18px;
background: rgba(8, 20, 45, .72);
backdrop-filter: blur(8px);
}
.resume-modal.open { display: flex; }
.resume-modal-box {
width: min(980px, 100%);
height: min(92vh, 980px);
border-radius: 28px;
overflow: hidden;
background: #ffffff;
border: 3px solid rgba(255, 191, 47, .78);
box-shadow: 0 30px 80px rgba(0,0,0,.32), 0 0 34px rgba(255,191,47,.20);
animation: fadeUp .28s ease both;
display: grid;
grid-template-rows: auto 1fr;
}
.resume-modal-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
background: linear-gradient(90deg, #ff3b3b 0%, #ffffff 48%, #2e7cff 100%);
color: #11376c;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
font-weight: 900;
}
.resume-modal-title {
font-size: clamp(.95rem, 2.5vw, 1.2rem);
text-transform: uppercase;
letter-spacing: .03em;
}
.resume-modal-actions {
display: flex;
gap: 8px;
align-items: center;
}
.resume-action {
appearance: none;
border: none;
border-radius: 999px;
padding: 10px 14px;
background: rgba(255,255,255,.88);
color: #11376c;
font-weight: 900;
cursor: pointer;
text-decoration: none;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
font-size: .82rem;
box-shadow: 0 8px 18px rgba(17,55,108,.14);
}
.resume-action:hover { transform: translateY(-1px); }
.resume-close { min-width: 42px; font-size: 1rem; }
.resume-preview-wrap {
width: 100%;
height: 100%;
overflow: auto;
background: #eef2f8;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 18px;
}
.resume-preview-img {
width: min(100%, 880px);
height: auto;
display: block;
border-radius: 12px;
box-shadow: 0 14px 34px rgba(17,55,108,.20);
background: white;
}
body.modal-lock { overflow: hidden; }
@media (max-width: 640px) {
.resume-modal { padding: 8px; }
.resume-modal-box { height: 94vh; border-radius: 20px; }
.resume-modal-top { flex-wrap: wrap; justify-content: center; text-align: center; }
.resume-modal-actions { width: 100%; justify-content: center; }
.resume-action { padding: 9px 12px; font-size: .72rem; }
.resume-preview-wrap { padding: 10px; }
}
/* Projects Popup Modal */
.projects-modal {
position: fixed;
inset: 0;
z-index: 10001;
display: none;
align-items: center;
justify-content: center;
padding: 18px;
background: rgba(8, 20, 45, .72);
backdrop-filter: blur(8px);
}
.projects-modal.open { display: flex; }
.projects-modal-box {
width: min(980px, 100%);
max-height: 92vh;
border-radius: 28px;
overflow: hidden;
background: rgba(255,255,255,.96);
border: 3px solid rgba(255, 191, 47, .78);
box-shadow: 0 30px 80px rgba(0,0,0,.32), 0 0 34px rgba(255,191,47,.20);
animation: fadeUp .28s ease both;
display: grid;
grid-template-rows: auto 1fr;
}
.projects-modal-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px;
background: linear-gradient(90deg, #ff3b3b 0%, #ffffff 48%, #2e7cff 100%);
color: #11376c;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
font-weight: 900;
}
.projects-modal-title {
font-size: clamp(1rem, 2.8vw, 1.35rem);
text-transform: uppercase;
letter-spacing: .04em;
}
.projects-close {
appearance: none;
border: none;
border-radius: 999px;
padding: 10px 16px;
background: rgba(255,255,255,.88);
color: #11376c;
font-weight: 900;
cursor: pointer;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
font-size: 1rem;
box-shadow: 0 8px 18px rgba(17,55,108,.14);
}
.projects-content {
overflow: auto;
padding: 20px;
background:
radial-gradient(circle at 10% 10%, rgba(255,59,59,.08), transparent 24%),
radial-gradient(circle at 90% 0%, rgba(46,124,255,.10), transparent 26%),
linear-gradient(180deg, #fffdf5, #f7fbff);
}
.project-grid {
display: grid;
gap: 16px;
}
.project-card {
border-radius: 24px;
background: linear-gradient(180deg, #ffffff, #f5f9ff);
border: 2px solid rgba(17,55,108,.08);
box-shadow: 0 12px 26px rgba(17,55,108,.08);
padding: 18px;
color: #11376c;
transition: transform .2s ease, box-shadow .2s ease;
}
.project-card:hover {
transform: translateY(-2px);
box-shadow: 0 18px 34px rgba(17,55,108,.13);
}
.project-card h3 {
margin: 0 0 8px;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
font-size: clamp(1.05rem, 3vw, 1.35rem);
color: #11376c;
text-transform: uppercase;
letter-spacing: .02em;
}
.project-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 10px 0 12px;
}
.project-tag {
display: inline-flex;
padding: 7px 10px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(255,59,59,.12), rgba(46,124,255,.12));
color: #11376c;
font-size: .78rem;
font-weight: 900;
text-transform: uppercase;
letter-spacing: .03em;
}
.project-card p {
margin: 0;
font-family: 'Trebuchet MS', Arial, sans-serif;
font-size: 1rem;
line-height: 1.6;
font-weight: 800;
color: #11376c;
}
.project-note {
margin-top: 16px;
padding: 14px 16px;
border-radius: 20px;
background: linear-gradient(90deg, rgba(255,59,59,.08), rgba(255,255,255,.86), rgba(46,124,255,.10));
border: 1px solid rgba(17,55,108,.08);
font-family: 'Trebuchet MS', Arial, sans-serif;
font-weight: 900;
line-height: 1.55;
color: #11376c;
text-align: center;
}
@media (max-width: 640px) {
.projects-modal { padding: 8px; }
.projects-modal-box { max-height: 94vh; border-radius: 20px; }
.projects-modal-top { text-align: center; }
.projects-content { padding: 14px; }
.project-card { padding: 16px; border-radius: 20px; }
}
.projects-detail-box { width: min(1080px, 100%); }
.project-open-btn,
.project-back-btn,
.project-download-btn {
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 14px;
padding: 12px 16px;
border-radius: 999px;
border: none;
background: linear-gradient(135deg,#ff3b3b,#2e7cff);
color: white;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
font-weight: 900;
text-decoration: none;
cursor: pointer;
box-shadow: 0 10px 22px rgba(17,55,108,.14);
}
.project-back-btn {
margin-top: 0;
margin-bottom: 14px;
background: linear-gradient(135deg,#ffffff,#dbeafe);
color: #11376c;
}
.project-action-row {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 14px 0 16px;
}
.project-detail h2 {
margin: 0 0 8px;
font-size: clamp(1.5rem, 5vw, 2.35rem);
line-height: 1;
text-transform: uppercase;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
color: #11376c;
}
.project-lead {
margin: 0 0 12px;
font-family: 'Trebuchet MS', Arial, sans-serif;
font-weight: 900;
color: #11376c;
line-height: 1.55;
}
.project-detail-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 16px 0;
}
.project-info-block {
border-radius: 20px;
padding: 15px;
background: rgba(255,255,255,.86);
border: 1px solid rgba(17,55,108,.08);
box-shadow: 0 8px 18px rgba(17,55,108,.06);
}
.project-info-block h4 {
margin: 0 0 7px;
color: #11376c;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
text-transform: uppercase;
}
.project-info-block p {
margin: 0;
font-family: 'Trebuchet MS', Arial, sans-serif;
line-height: 1.55;
font-weight: 800;
color: #11376c;
}
.code-window,
.terminal-card {
margin-top: 14px;
border-radius: 20px;
overflow: hidden;
border: 1px solid rgba(17,55,108,.12);
box-shadow: 0 12px 26px rgba(17,55,108,.10);
background: #08142d;
}
.code-title,
.terminal-title {
padding: 10px 14px;
background: linear-gradient(90deg, #ff3b3b, #ffffff, #2e7cff);
color: #11376c;
font-family: 'Arial Black', 'Trebuchet MS', sans-serif;
font-weight: 900;
}
.code-window pre,
.terminal-card pre {
margin: 0;
padding: 14px;
overflow: auto;
color: #eaf2ff;
font-family: Menlo, Monaco, Consolas, monospace;
font-size: .82rem;
line-height: 1.5;
max-height: 360px;
}
@media (max-width: 760px) {
.project-detail-grid { grid-template-columns: 1fr; }
.project-action-row { flex-direction: column; }
.project-download-btn { width: 100%; }
}
</style>
</head>
<body>
<canvas id="fxLayer" class="fx-layer"></canvas>
<div class="bg"></div>
<div id="projectsModal" class="projects-modal" aria-hidden="true">
<div class="projects-modal-box projects-detail-box" role="dialog" aria-modal="true" aria-label="Joe Wolff Projects">
<div class="projects-modal-top">
<div class="projects-modal-title">Projects</div>
<button class="projects-close" type="button" onclick="closeProjectsModal()">✕</button>
</div>
<div class="projects-content">
<div id="projectListView">
<div class="project-grid">
<div class="project-card">
<h3>This Resume Website</h3>
<div class="project-tags">
<span class="project-tag">HTML</span>
<span class="project-tag">CSS</span>
<span class="project-tag">JavaScript</span>
<span class="project-tag">AI-Assisted Development</span>
</div>
<p>This website was built as an interactive resume and portfolio. It combines my personal story, resume, projects, contact button, animations, and popup features into one custom-coded site.</p>
<button class="project-open-btn" type="button" onclick="openProjectDetail('website')">Open Project →</button>
</div>
<div class="project-card">
<h3>Automated Forex Trading Bot</h3>
<div class="project-tags">
<span class="project-tag">Python</span>
<span class="project-tag">APIs</span>
<span class="project-tag">Real-Time Market Data</span>
<span class="project-tag">Automation</span>
</div>
<p>This Python project uses APIs, real-time market data, multi-timeframe analysis, and predefined rules to scan currency pairs and manage trades automatically.</p>
<button class="project-open-btn" type="button" onclick="openProjectDetail('bot')">Open Project →</button>
</div>
<div class="project-card">
<h3>What Is Money</h3>
<div class="project-tags">
<span class="project-tag">E-Commerce</span>
<span class="project-tag">Photoshop</span>
<span class="project-tag">Printful</span>
<span class="project-tag">Squarespace</span>
<span class="project-tag">Ads</span>
</div>
<p>My first e-commerce project and one of my most fun, sales-oriented projects. I designed apparel, built a store, used Printful for fulfillment, and experimented with online advertising.</p>
<button class="project-open-btn" type="button" onclick="openProjectDetail('money')">Open Project →</button>
</div>
</div>
</div>
<div id="websiteDetail" class="project-detail" hidden>
<button class="project-back-btn" type="button" onclick="backToProjects()">← Back to Projects</button>
<h2>This Resume Website</h2>
<p class="project-lead">This is the code for this website.</p>
<div class="project-action-row">
<a class="project-download-btn" href="code-viewer.html" target="_blank">View HTML Code</a>
</div>
<div class="project-detail-grid">
<div class="project-info-block">
<h4>What This Is</h4>
<p>This opens a clean code viewer page showing the HTML, CSS, and JavaScript used to build the site.</p>
</div>
<div class="project-info-block">
<h4>Built With</h4>
<p>HTML, CSS, JavaScript, and AI-assisted development.</p>
</div>
</div>
</div>
<div id="botDetail" class="project-detail" hidden>
<button class="project-back-btn" type="button" onclick="backToProjects()">← Back to Projects</button>
<h2>Automated Forex Trading Bot</h2>
<p class="project-lead">A Python automation project that connects market data, APIs, strategy logic, position sizing, and trade management.</p>
<div class="project-action-row">
<a class="project-download-btn" href="forex_bot_source.zip" download>Download Bot Code</a>
<a class="project-download-btn" href="bot.py" download>View bot.py</a>
<a class="project-download-btn" href="TRADE_BOT_START.txt" download>Startup Guide</a>
</div>
<div class="project-detail-grid">
<div class="project-info-block">
<h4>Core Idea</h4>
<p>The bot scans currency pairs, checks multiple timeframes, scores trade setups, places market orders, and manages exits using predefined risk rules.</p>
</div>
<div class="project-info-block">
<h4>Features</h4>
<p>OANDA API connection, 4H/30M/10M analysis, EMA/ADX indicators, notional sizing, trade logs, cooldowns, take-profit logic, and risk management.</p>
</div>
<div class="project-info-block">
<h4>Safety Note</h4>
<p>API keys and .env files are intentionally not included. The downloadable code is for review only and should not expose private credentials.</p>
</div>
</div>
<div class="terminal-card">
<div class="terminal-title">Startup Example</div>
<pre><code>cd ~/Desktop/oanda-trend-bot
source venv/bin/activate
caffeinate -dimsu python bot.py</code></pre>
</div>
<div class="code-window">
<div class="code-title">Code Sample</div>
<pre><code>
except Exception as exc:
print("Could not save trade states:", exc)
trade_states = load_trade_states()
# =========================
# CONFIG
# =========================
load_dotenv()
API_KEY = os.getenv("OANDA_API_KEY")
ACCOUNT_ID = os.getenv("OANDA_ACCOUNT_ID")
ENV = os.getenv("OANDA_ENV", "practice")
PAIRS = ["USD_CAD", "EUR_CAD", "USD_DKK", "EUR_USD"]
STARTING_BALANCE = float(os.getenv("STARTING_BALANCE", "100000"))
TRADE_USD_NOTIONAL = float(os.getenv("TRADE_USD_NOTIONAL", "85000"))
PAIR_NOTIONAL_CAPS = {
"USD_DKK": 40000
}
PROFIT_TARGET_PCT = 0.0333
"description"
])
with open(CHANGE_LOG_FILE, "a", newline="") as f:
writer = csv.writer(f)
writer.writerow([
now_utc().isoformat(),
BOT_VERSION,
description
])
def log_version_once():
previous_version = None
if os.path.exists(VERSION_MARKER_FILE):
with open(VERSION_MARKER_FILE, "r") as f:
previous_version = f.read().strip()
if previous_version != BOT_VERSION:
log_code_change(
"Added trades.csv logging, code_changes.csv logging, partial TP, 6% max loss, consolidation filter, and USD notional sizing."
)
with open(VERSION_MARKER_FILE, "w") as f:
f.write(BOT_VERSION)
# =========================
# DATA / INDICATORS
# =========================
def get_candles(pair, granularity, count=250):
params = {
"count": count,
"granularity": granularity,
"price": "M"
}
r = instruments.InstrumentsCandles(instrument=pair, params=params)
client.request(r)
if units_to_close <= 0:
print("Partial close skipped: units_to_close <= 0")
return None
if direction == "long":
data = {"longUnits": str(units_to_close)}
else:
data = {"shortUnits": str(units_to_close)}
r = positions.PositionClose(
ACCOUNT_ID,
instrument=pair,
data=data
)
client.request(r)
print("PARTIAL POSITION CLOSED:", pair, direction, units_to_close)
return r.response
def is_friday_close_time():
"""
Returns True on Friday at or after 4:45 PM New York time.
Used to close all trades before forex market close.
"""
ny_time = datetime.now(ZoneInfo("America/New_York"))
is_friday = ny_time.weekday() == 4
after_cutoff = (
ny_time.hour > FRIDAY_CLOSE_HOUR_NY or
(
ny_time.hour == FRIDAY_CLOSE_HOUR_NY and
ny_time.minute >= FRIDAY_CLOSE_MINUTE_NY
</code></pre>
</div>
</div>
<div id="moneyDetail" class="project-detail" hidden>
<button class="project-back-btn" type="button" onclick="backToProjects()">← Back to Projects</button>
<h2>What Is Money</h2>
<p class="project-lead">My first e-commerce project, and one of my most fun and sales-oriented projects.</p>
<div class="project-detail-grid">
<div class="project-info-block">
<h4>What I Built</h4>
<p>I created a simple online apparel store using Squarespace, designed products in Photoshop, and used Printful to handle production, transactions, and delivery.</p>
</div>
<div class="project-info-block">
<h4>Sales & Marketing</h4>
<p>I created advertisements and tested online promotion. Even though it did not become highly profitable, it gave me real experience with branding, selling, and digital marketing.</p>
</div>
<div class="project-info-block">
<h4>What I Learned</h4>
<p>I learned how design, product fulfillment, ads, website flow, and customer attention all connect in an online business.</p>
</div>
</div>
<div class="project-note">
Add your old screenshots, Snapchat ads, product mockups, and Photoshop designs here next.
</div>
</div>
</div>
</div>
</div>
<main class="wrap">
<section class="hero">
<div class="hero-years" style="font-weight:900; font-size:1.1rem; margin-bottom:4px; color:#11376c; text-align:center; width:100%; display:block;">Personable • Positive • Problem Solver</div>
<div class="mini-logo"><img src="[embedded image data shortened]" alt="Joe Wolff professional headshot"></div>
<h1>Joe Wolff</h1>
<div class="hero-subtitle">Interactive Resume & Portfolio</div>
<div class="hero-buttons">
<button class="btn primary" type="button" onclick="openResumeModal()">Resume</button>
<a class="btn" href="#contact">Contact</a>
<button class="btn" type="button" onclick="openProjectsModal()">Projects</button>
</div>
</section>
<section class="card" id="about">
<div class="section-title">About Me</div>
<div class="resume-copy">
<p>I have lived in the Kansas City area my entire life, growing up in communities such as Stillwell, Leawood, Mission Hills, and Louisburg. I attended both Blue Valley High School and Blue Valley North, where I spent much of my time as a competitive swimmer. I swam year-round from the age of 11 until I went to college, and it became a major part of my life throughout my childhood and high school years.</p>
<p>After high school, I attended Pittsburg State University, where I earned a Bachelor of Science degree in Political Science with an emphasis in Law and a minor in History. While in college, I was active in Greek life and met my girlfriend of five years. I take a great deal of pride in the relationships I have with my family, friends, and the people closest to me.</p>
<p>Outside of work and school, I enjoy spending time with friends and family, attending social events, and helping out on my grandmother’s farm in Archie, Missouri. When traveling, I prefer places where I can grow my scuba diving resume.</p>
<p>My favorite kind of food is Mexican, and my favorite holiday is the Fourth of July.</p>
</div>
</section>
<section class="card" id="experience">
<div class="section-title">Resume</div>
<div class="resume-copy">
<p>I got my first job at 14 years old at Johnson Farms Pumpkin Patch, where I worked for one season in 2015. At 15, I began working over the summer at Seasonal Solutions, a landscaping company, starting as a weed puller. I continued working there seasonally during winters and summers when I could, from 2016 through college in 2023.</p>
<p>For my undergraduate studies, I graduated in 2025 from Pittsburg State University with a Bachelor of Science degree in Political Science, with an emphasis in Law, and a minor in History.</p>
<p>After graduation, I began working at Menards in Joplin, Missouri. I am now looking for a long-term position and company where I can continue to grow my career.</p>
</div>
</section>
<section class="card" id="projects">
<div class="section-title">Technology & Projects</div>
<div class="resume-copy">
<p>In college, I developed an interest in the stock market, which surprisingly started from reading the Old Farmer’s Almanac. That curiosity quickly grew into an interest in technical and fundamental analysis across all types of markets.</p>
<p>Somehow, that led me to creating my first website, “What Is Money,” an online store where I sold apparel that I designed myself using Photoshop. I used a third-party company called Printful to handle everything from processing orders to manufacturing and shipping products through my Squarespace store. While the business was not as successful as I had hoped, it gave me my first experience with website design, e-commerce, digital marketing, and running a small business.</p>
<p>Since then, my interest in technology has expanded into Web3, cryptocurrency, and the ways technology can connect creativity with financial markets. This eventually led me to coding and discovering the capabilities of AI beyond simply asking questions.</p>
<p>For example, this website is not just a template. It was built by working directly with HTML, CSS, and JavaScript code, using AI as a tool to learn, troubleshoot, and customize the site to fit my vision. I have also developed an automated Forex trading bot written in Python that uses real-time market data and APIs to analyze currency pairs and execute trades based on predefined rules.</p>
<p>These projects combine my interest in financial markets, technology, and problem-solving while giving me experience with coding, automation, data analysis, and adapting to new technologies. I believe this is one of my strongest qualities: using AI not just to find answers, but to discover new ways of solving problems and improving how work gets done.</p>
</div>
</section>
<section class="card ca" id="contact">
<button class="copy-btn" onclick="copyEmail()">
<span>★ Contact Me Today ★</span>
</button>
<code id="email">joewolff203@gmail.com</code>
<div class="small">I learn. I sell. I solve problems.</div>
</section>
</main>
<script>
function updateCountdown() {}
const fxCanvas = document.getElementById('fxLayer');
const fxCtx = fxCanvas ? fxCanvas.getContext('2d') : null;
let particles = [];
let rings = [];
let audioCtx = null;
function resizeCanvas() {
if (!fxCanvas || !fxCtx) return;
const dpr = window.devicePixelRatio || 1;
fxCanvas.width = Math.floor(window.innerWidth * dpr);
fxCanvas.height = Math.floor(window.innerHeight * dpr);
fxCanvas.style.width = window.innerWidth + 'px';
fxCanvas.style.height = window.innerHeight + 'px';
fxCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function ensureAudio() {
try {
if (!audioCtx) {
const Ctx = window.AudioContext || window.webkitAudioContext;
if (!Ctx) return null;
audioCtx = new Ctx();
}
if (audioCtx.state === 'suspended') audioCtx.resume();
return audioCtx;
} catch (e) {
return null;
}
}
function playFireworkSound(kind) {
const ctx = ensureAudio();
if (!ctx) return;
const now = ctx.currentTime;
const launchAt = now;
const boomAt = now + 0.01 // immediate 0.26;
const duration = 2.25;
const master = ctx.createGain();
master.connect(ctx.destination);
master.gain.value = 1.1;
// Main deep boom
const boom = ctx.createOscillator();
const boomGain = ctx.createGain();
boom.type = 'sine';
boom.frequency.setValueAtTime(58, boomAt);
boom.frequency.exponentialRampToValueAtTime(26, boomAt + 0.7);
boomGain.gain.setValueAtTime(0.0001, boomAt);
boomGain.gain.exponentialRampToValueAtTime(0.85, boomAt + 0.01);
boomGain.gain.exponentialRampToValueAtTime(0.0001, boomAt + 0.78);
boom.connect(boomGain);
boomGain.connect(master);
boom.start(boomAt);
boom.stop(boomAt + 0.8);
// Sub layer for chest hit
const sub = ctx.createOscillator();
const subGain = ctx.createGain();
sub.type = 'sine';
sub.frequency.setValueAtTime(38, boomAt);
sub.frequency.exponentialRampToValueAtTime(22, boomAt + 0.95);
subGain.gain.setValueAtTime(0.0001, boomAt);
subGain.gain.exponentialRampToValueAtTime(0.45, boomAt + 0.015);
subGain.gain.exponentialRampToValueAtTime(0.0001, boomAt + 1.0);
sub.connect(subGain);
subGain.connect(master);
sub.start(boomAt);
sub.stop(boomAt + 1.02);
// Noise tail with lower, chunkier crackle
const bufferSize = Math.floor(ctx.sampleRate * duration);
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
const t = i / bufferSize;
const env = Math.pow(1 - t, 1.9);
const heavy = (Math.random() * 2 - 1) * (Math.random() > 0.9 ? 2.2 : 0.42);
data[i] = heavy * env;
}
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const hp = ctx.createBiquadFilter();
hp.type = 'highpass';
hp.frequency.setValueAtTime(650, boomAt);
const lp = ctx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.setValueAtTime(2600, boomAt);
const crackleGain = ctx.createGain();
crackleGain.gain.setValueAtTime(0.0001, boomAt);
crackleGain.gain.exponentialRampToValueAtTime(0.22, boomAt + 0.03);
crackleGain.gain.exponentialRampToValueAtTime(0.0001, boomAt + duration);
noise.connect(hp);
hp.connect(lp);
lp.connect(crackleGain);
crackleGain.connect(master);
noise.start(boomAt);
noise.stop(boomAt + duration);
}
function addRing(x, y, color, radius) {
rings.push({ x, y, color, radius, life: 48, maxLife: 48 });
}
function addStarBurst(x, y, count, palette, spread = 1) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const speed = (1.8 + Math.random() * 3.8) * spread;
particles.push({
x, y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed - Math.random() * 0.55,
size: 2.8 + Math.random() * 5.2,
life: 92 + Math.floor(Math.random() * 54),
maxLife: 146,
color: palette[Math.floor(Math.random() * palette.length)],
shape: Math.random() > 0.18 ? 'spark' : 'star',
twinkle: Math.random() * Math.PI * 2
});
}
}
function addFallingSparkTrail(x, y, count, palette) {
for (let i = 0; i < count; i++) {
particles.push({
x: x + (Math.random() - 0.5) * 30,
y: y + (Math.random() - 0.5) * 14,
vx: (Math.random() - 0.5) * 1.6,
vy: Math.random() * -2.2,
size: 2.2 + Math.random() * 3.0,
life: 118 + Math.floor(Math.random() * 54),
maxLife: 176,
color: palette[Math.floor(Math.random() * palette.length)],
shape: 'ember',
twinkle: Math.random() * Math.PI * 2
});
}
}
function drawStar(ctx, x, y, r, color, alpha) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(Math.PI / 2);
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.beginPath();
for (let i = 0; i < 5; i++) {
const outer = r;
const inner = r * 0.45;
ctx.lineTo(Math.cos((18 + i * 72) * Math.PI / 180) * outer, -Math.sin((18 + i * 72) * Math.PI / 180) * outer);
ctx.lineTo(Math.cos((54 + i * 72) * Math.PI / 180) * inner, -Math.sin((54 + i * 72) * Math.PI / 180) * inner);
}
ctx.closePath();
ctx.fill();
ctx.restore();
}
function triggerFireworks(kind, sourceEl) {
const w = window.innerWidth;
const h = window.innerHeight;
const patriotic = ['#ff3b3b', '#ffffff', '#2e7cff', '#ffbf2f'];
const goldHeavy = ['#ffbf2f', '#ffd76a', '#ffffff', '#ff3b3b'];
const palette = kind === 'copy' ? goldHeavy : patriotic;
const bursts = kind === 'copy'
? [
[w * 0.12, h * 0.18, 68, 1.62],
[w * 0.88, h * 0.18, 68, 1.62],
[w * 0.50, h * 0.14, 52, 1.42],
[w * 0.26, h * 0.34, 34, 1.12],
[w * 0.74, h * 0.34, 34, 1.12]
]
: [
[w * 0.16, h * 0.20, 52, 1.42],
[w * 0.84, h * 0.20, 52, 1.42],
[w * 0.50, h * 0.15, 36, 1.18]
];
bursts.forEach(([x, y, count, spread], idx) => {
setTimeout(() => {
addRing(x, y, palette[idx % palette.length], 36 + idx * 8);
addStarBurst(x, y, count, palette, spread);
addStarBurst(x + (Math.random() - 0.5) * 22, y + (Math.random() - 0.5) * 12, Math.floor(count * 0.35), palette, spread * 0.72);
addFallingSparkTrail(x, y, Math.floor(count * 0.82), palette);
}, idx * 180);
});
if (sourceEl) {
const rect = sourceEl.getBoundingClientRect();
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
addRing(x, y, '#ffffff', 24);
addStarBurst(x, y, 28, palette, 1.0);
}
playFireworkSound(kind);
}
function animateParticles() {
if (!fxCtx) return;
fxCtx.clearRect(0, 0, window.innerWidth, window.innerHeight);
rings = rings.filter(r => r.life > 0);
for (const r of rings) {
const alpha = r.life / r.maxLife;
fxCtx.save();
fxCtx.globalAlpha = alpha * 0.7;
fxCtx.strokeStyle = r.color;
fxCtx.lineWidth = 4.2;
fxCtx.beginPath();
fxCtx.arc(r.x, r.y, r.radius + (r.maxLife - r.life) * 5.4, 0, Math.PI * 2);
fxCtx.stroke();
fxCtx.restore();
r.life -= 1;
}
particles = particles.filter(p => p.life > 0);
for (const p of particles) {
const prevX = p.x;
const prevY = p.y;
p.x += p.vx;
p.y += p.vy;
p.vy += p.shape === 'ember' ? 0.032 : 0.022;
p.vx *= 0.996;
p.life -= 1;
const alphaBase = Math.max(p.life / p.maxLife, 0);
const twinkle = 0.82 + 0.18 * Math.sin((p.twinkle || 0) + (p.maxLife - p.life) * 0.18);
const alpha = alphaBase * twinkle;
fxCtx.save();
fxCtx.globalAlpha = alpha * 0.25;
fxCtx.strokeStyle = p.color;
fxCtx.lineWidth = Math.max(1, p.size * 0.35);
fxCtx.beginPath();
fxCtx.moveTo(prevX, prevY);
fxCtx.lineTo(p.x, p.y);
fxCtx.stroke();
fxCtx.restore();
if (p.shape === 'star') {
drawStar(fxCtx, p.x, p.y, p.size + 0.8, p.color, alpha);
} else {
fxCtx.save();
fxCtx.globalAlpha = p.shape === 'ember' ? alpha * 0.92 : alpha;
fxCtx.shadowBlur = 14;
fxCtx.shadowColor = p.color;
fxCtx.fillStyle = p.color;
fxCtx.beginPath();
fxCtx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
fxCtx.fill();
fxCtx.restore();
}
}
requestAnimationFrame(animateParticles);
}
function copyEmail() {
const el = document.getElementById('email');
const text = el.innerText;
navigator.clipboard.writeText(text).catch(() => {});
const btn = document.querySelector('.copy-btn');
const label = document.querySelector('.copy-btn span');
const original = label.textContent;
btn.style.animation = 'clickFlash .35s ease';
label.textContent = 'Email Copied ✅';
if (typeof triggerFireworks === 'function') {
triggerFireworks('copy', btn);
}
setTimeout(() => {
label.textContent = original;
btn.style.animation = 'caPulse 2.2s ease-in-out infinite';
}, 1200);
}
resizeCanvas();
animateParticles();
window.addEventListener('resize', resizeCanvas);
window.addEventListener('pointerdown', ensureAudio, { once: true });
window.addEventListener('load', () => {
const heroButtons = document.querySelectorAll('.hero-buttons .btn');
heroButtons.forEach((btn) => {
btn.addEventListener('click', () => {
triggerFireworks('button', btn);
});
});
});
function smoothScrollToElement(target, duration = 1200) {
if (!target) return;
const startY = window.pageYOffset;
const targetRect = target.getBoundingClientRect();
const targetY = startY + targetRect.top - (window.innerHeight * 0.18);
const distance = targetY - startY;
const startTime = performance.now();
function easeInOutCubic(t) {
return t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2;
}
function step(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = easeInOutCubic(progress);
window.scrollTo(0, startY + distance * eased);
if (progress < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
function openResumeModal() {
const modal = document.getElementById('resumeModal');
if (!modal) return;
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-lock');
showProjectList();
if (typeof triggerFireworks === 'function') {
triggerFireworks('button', document.querySelector('.hero-buttons .btn'));
}
}
function closeResumeModal() {
const modal = document.getElementById('resumeModal');
if (!modal) return;
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-lock');
}
function printResume() {
const printWindow = window.open('', '_blank');
if (!printWindow) return;
printWindow.document.write(`
<html>
<head><title>Joe Wolff Resume</title></head>
<body style="margin:0;text-align:center;background:#eef2f8;">
<img src="${document.querySelector('.resume-preview-img').src}" style="width:100%;max-width:900px;">
<script>
window.onload = function(){ window.print(); };
<\/script>
</body>
</html>
`);
printWindow.document.close();
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') closeResumeModal();
});
document.addEventListener('click', function(e) {
const modal = document.getElementById('resumeModal');
if (modal && e.target === modal) closeResumeModal();
});
function showProjectList() {
const list = document.getElementById('projectListView');
const details = document.querySelectorAll('.project-detail');
if (list) list.hidden = false;
details.forEach(d => d.hidden = true);
}
function openProjectDetail(project) {
const list = document.getElementById('projectListView');
const details = document.querySelectorAll('.project-detail');
if (list) list.hidden = true;
details.forEach(d => d.hidden = true);
const target = document.getElementById(project + 'Detail');
if (target) target.hidden = false;
}
function backToProjects() {
showProjectList();
}
function openProjectsModal() {
const modal = document.getElementById('projectsModal');
if (!modal) return;
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-lock');
if (typeof triggerFireworks === 'function') {
triggerFireworks('button', document.querySelector('.hero-buttons .btn:nth-child(3)'));
}
}
function closeProjectsModal() {
const modal = document.getElementById('projectsModal');
if (!modal) return;
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-lock');
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeProjectsModal();
}
});
document.addEventListener('click', function(e) {
const modal = document.getElementById('projectsModal');
if (modal && e.target === modal) closeProjectsModal();
});
</script>
</body>
</html>