Commit d31f77fc authored by HungTQ's avatar HungTQ

style(ui): enhance portfolio motion design

parent d288b585
:root { :root {
--ink: #111827; --ink: #f6fbff;
--muted: #5b6678; --muted: #a7b7c8;
--paper: #f5f0e8; --dim: #6f8194;
--surface: #fffaf1; --paper: #061018;
--line: #ded2c1; --surface: rgba(12, 26, 38, 0.78);
--accent: #105c6d; --surface-strong: rgba(16, 35, 49, 0.96);
--accent-strong: #0d4855; --line: rgba(139, 213, 255, 0.18);
--signal: #d64d2a; --line-strong: rgba(139, 213, 255, 0.38);
--night: #172025; --accent: #42d9ff;
--shadow: 0 24px 70px rgba(28, 38, 43, 0.16); --accent-strong: #9df2ff;
--signal: #ff6b45;
--signal-soft: #ffb199;
--mint: #80ffcc;
--shadow: 0 34px 110px rgba(0, 0, 0, 0.48);
} }
* { * {
...@@ -22,12 +26,44 @@ html { ...@@ -22,12 +26,44 @@ html {
body { body {
margin: 0; margin: 0;
background: background:
linear-gradient(90deg, rgba(17, 24, 39, 0.05) 1px, transparent 1px), linear-gradient(135deg, rgba(66, 217, 255, 0.10), transparent 32%),
linear-gradient(rgba(17, 24, 39, 0.04) 1px, transparent 1px), linear-gradient(225deg, rgba(255, 107, 69, 0.10), transparent 34%),
radial-gradient(circle at 50% 0%, rgba(128, 255, 204, 0.07), transparent 34%),
var(--paper); var(--paper);
background-size: 42px 42px;
color: var(--ink); color: var(--ink);
font-family: "Segoe UI", Candara, "Trebuchet MS", sans-serif; font-family: "Segoe UI", Candara, "Trebuchet MS", sans-serif;
overflow-x: hidden;
}
body::before {
animation: drift 22s linear infinite;
background-image:
linear-gradient(rgba(139, 213, 255, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(139, 213, 255, 0.05) 1px, transparent 1px);
background-size: 42px 42px;
content: "";
inset: 0;
pointer-events: none;
position: fixed;
z-index: -2;
}
body::after {
background:
repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.025) 0,
rgba(255, 255, 255, 0.025) 1px,
transparent 1px,
transparent 4px
);
content: "";
inset: 0;
mix-blend-mode: overlay;
opacity: 0.55;
pointer-events: none;
position: fixed;
z-index: -1;
} }
a { a {
...@@ -41,17 +77,28 @@ input { ...@@ -41,17 +77,28 @@ input {
main { main {
min-height: 100vh; min-height: 100vh;
position: relative;
}
.ambient-grid {
background:
linear-gradient(115deg, transparent 0 38%, rgba(66, 217, 255, 0.10) 38% 39%, transparent 39% 100%),
linear-gradient(65deg, transparent 0 60%, rgba(255, 107, 69, 0.10) 60% 61%, transparent 61% 100%);
inset: 0;
pointer-events: none;
position: fixed;
z-index: -1;
} }
.topbar { .topbar {
align-items: center; align-items: center;
backdrop-filter: blur(18px); backdrop-filter: blur(22px);
background: rgba(245, 240, 232, 0.84); background: rgba(6, 16, 24, 0.74);
border-bottom: 1px solid rgba(222, 210, 193, 0.8); border-bottom: 1px solid var(--line);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
left: 0; left: 0;
padding: 18px clamp(18px, 5vw, 64px); padding: 16px clamp(18px, 5vw, 72px);
position: sticky; position: sticky;
right: 0; right: 0;
top: 0; top: 0;
...@@ -60,54 +107,73 @@ main { ...@@ -60,54 +107,73 @@ main {
.brand { .brand {
align-items: center; align-items: center;
background: var(--night); background: linear-gradient(135deg, var(--accent), var(--mint));
border-radius: 8px; border-radius: 8px;
color: #fffaf1; color: #061018;
display: inline-flex; display: inline-flex;
font-weight: 900; font-weight: 950;
height: 40px; height: 42px;
justify-content: center; justify-content: center;
letter-spacing: 0.08em; letter-spacing: 0.08em;
text-decoration: none; text-decoration: none;
width: 48px; width: 52px;
} }
.nav-links { .nav-links {
display: flex; display: flex;
gap: 22px; gap: 8px;
} }
.nav-links a { .nav-links a {
border: 1px solid transparent;
border-radius: 8px;
color: var(--muted); color: var(--muted);
font-size: 14px; font-size: 13px;
font-weight: 800; font-weight: 850;
padding: 9px 12px;
text-decoration: none; text-decoration: none;
transition: border-color 180ms ease, color 180ms ease, background 180ms ease;
} }
.nav-links a:hover { .nav-links a:hover {
background: rgba(139, 213, 255, 0.08);
border-color: var(--line);
color: var(--ink); color: var(--ink);
} }
.hero { .hero {
display: grid; display: grid;
gap: clamp(28px, 5vw, 72px); gap: clamp(30px, 5vw, 86px);
grid-template-columns: minmax(0, 1.04fr) minmax(320px, 0.8fr); grid-template-columns: minmax(0, 1.02fr) minmax(340px, 0.82fr);
min-height: calc(100vh - 77px); min-height: calc(100vh - 75px);
padding: clamp(48px, 7vw, 92px) clamp(18px, 5vw, 64px) 56px; overflow: hidden;
padding: clamp(58px, 8vw, 110px) clamp(18px, 5vw, 72px) 68px;
position: relative;
}
.hero::before {
animation: sweep 8s ease-in-out infinite;
background: linear-gradient(90deg, transparent, rgba(66, 217, 255, 0.16), transparent);
content: "";
height: 1px;
left: -20%;
position: absolute;
top: 17%;
width: 140%;
} }
.hero-copy { .hero-copy {
align-self: center; align-self: center;
max-width: 760px; max-width: 790px;
} }
.eyebrow, .eyebrow,
.section-kicker { .section-kicker {
color: var(--accent); color: var(--accent);
font-size: 13px; font-size: 13px;
font-weight: 900; font-weight: 950;
letter-spacing: 0.12em; letter-spacing: 0.14em;
margin: 0 0 14px; margin: 0 0 16px;
text-transform: uppercase; text-transform: uppercase;
} }
...@@ -120,16 +186,30 @@ p { ...@@ -120,16 +186,30 @@ p {
h1 { h1 {
font-family: Georgia, "Times New Roman", serif; font-family: Georgia, "Times New Roman", serif;
font-size: clamp(56px, 10vw, 124px); font-size: clamp(58px, 10vw, 132px);
line-height: 0.88; line-height: 0.82;
margin-bottom: 20px; margin-bottom: 24px;
max-width: 900px; max-width: 980px;
}
h1 span {
animation: titleRise 780ms cubic-bezier(0.2, 0.8, 0.2, 1) both;
display: block;
text-shadow: 0 0 34px rgba(66, 217, 255, 0.18);
}
h1 span:nth-child(2) {
animation-delay: 80ms;
}
h1 span:nth-child(3) {
animation-delay: 160ms;
} }
h2 { h2 {
font-family: Georgia, "Times New Roman", serif; font-family: Georgia, "Times New Roman", serif;
font-size: clamp(34px, 5vw, 64px); font-size: clamp(34px, 5vw, 68px);
line-height: 1; line-height: 0.98;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -140,24 +220,24 @@ h3 { ...@@ -140,24 +220,24 @@ h3 {
} }
.role { .role {
color: var(--signal); color: var(--signal-soft);
font-size: clamp(22px, 3vw, 32px); font-size: clamp(22px, 3vw, 34px);
font-weight: 900; font-weight: 950;
margin-bottom: 18px; margin-bottom: 18px;
} }
.summary { .summary {
color: var(--muted); color: var(--muted);
font-size: clamp(17px, 2vw, 22px); font-size: clamp(17px, 2vw, 22px);
line-height: 1.55; line-height: 1.58;
max-width: 720px; max-width: 760px;
} }
.hero-actions { .hero-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 12px; gap: 12px;
margin: 32px 0; margin: 34px 0;
} }
.primary-action, .primary-action,
...@@ -165,29 +245,36 @@ h3 { ...@@ -165,29 +245,36 @@ h3 {
align-items: center; align-items: center;
border-radius: 8px; border-radius: 8px;
display: inline-flex; display: inline-flex;
font-weight: 900; font-weight: 950;
gap: 8px; gap: 8px;
min-height: 48px; min-height: 50px;
padding: 0 18px; padding: 0 18px;
text-decoration: none; text-decoration: none;
transform: translateZ(0);
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
} }
.primary-action { .primary-action {
background: var(--accent); background: linear-gradient(135deg, var(--accent), var(--mint));
color: #fffaf1; box-shadow: 0 18px 46px rgba(66, 217, 255, 0.22);
color: #061018;
} }
.primary-action:hover { .primary-action:hover,
background: var(--accent-strong); .secondary-action:hover {
transform: translateY(-3px);
} }
.secondary-action { .secondary-action {
background: rgba(255, 250, 241, 0.8); background: rgba(12, 26, 38, 0.78);
border: 1px solid var(--line); border: 1px solid var(--line);
color: var(--ink); color: var(--ink);
} }
.signal-row { .signal-row,
.skill-cloud,
.stack-list,
.project-links {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 10px; gap: 10px;
...@@ -196,38 +283,85 @@ h3 { ...@@ -196,38 +283,85 @@ h3 {
.signal-row span, .signal-row span,
.skill-cloud span, .skill-cloud span,
.stack-list span { .stack-list span {
background: #fffaf1; background: rgba(12, 26, 38, 0.82);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
color: var(--accent-strong); color: var(--accent-strong);
display: inline-flex; display: inline-flex;
font-size: 13px; font-size: 13px;
font-weight: 900; font-weight: 950;
padding: 8px 10px; padding: 8px 10px;
} }
.hero-metrics {
display: grid;
gap: 10px;
grid-template-columns: repeat(3, minmax(0, 1fr));
margin-top: 28px;
max-width: 650px;
}
.hero-metrics div {
background: rgba(12, 26, 38, 0.66);
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px;
}
.hero-metrics strong,
.hero-metrics span {
display: block;
}
.hero-metrics strong {
color: var(--ink);
font-size: 22px;
margin-bottom: 5px;
}
.hero-metrics span {
color: var(--dim);
font-size: 12px;
font-weight: 850;
text-transform: uppercase;
}
.hero-visual { .hero-visual {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: center; justify-content: center;
min-height: 520px; min-height: 560px;
position: relative; position: relative;
} }
.device-halo {
animation: pulseGlow 4.6s ease-in-out infinite;
background:
conic-gradient(from 120deg, rgba(66, 217, 255, 0), rgba(66, 217, 255, 0.55), rgba(255, 107, 69, 0.45), rgba(66, 217, 255, 0));
border-radius: 40px;
filter: blur(30px);
height: 78%;
opacity: 0.62;
position: absolute;
transform: rotate(8deg);
width: min(88%, 440px);
}
.phone-frame { .phone-frame {
background: #1b252b; animation: deviceFloat 5.5s ease-in-out infinite;
border: 12px solid #1b252b; background: #0a151e;
border-radius: 30px; border: 12px solid #111f2a;
box-shadow: var(--shadow); border-radius: 32px;
max-width: 360px; box-shadow: var(--shadow), inset 0 0 0 1px rgba(255, 255, 255, 0.06);
max-width: 365px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
transform: rotate(2deg); transform: rotate(2deg);
width: min(100%, 360px); width: min(100%, 365px);
} }
.phone-speaker { .phone-speaker {
background: #303c42; background: #2c3c47;
border-radius: 999px; border-radius: 999px;
height: 6px; height: 6px;
left: 50%; left: 50%;
...@@ -235,58 +369,78 @@ h3 { ...@@ -235,58 +369,78 @@ h3 {
top: 12px; top: 12px;
transform: translateX(-50%); transform: translateX(-50%);
width: 76px; width: 76px;
z-index: 3;
}
.screen-scan {
animation: scan 3.6s ease-in-out infinite;
background: linear-gradient(180deg, transparent, rgba(66, 217, 255, 0.30), transparent);
height: 24%;
left: 0;
pointer-events: none;
position: absolute;
right: 0;
top: -30%;
z-index: 2; z-index: 2;
} }
.phone-frame img { .phone-frame img {
display: block; display: block;
filter: saturate(0.92) contrast(1.04); filter: saturate(1.02) contrast(1.07);
width: 100%; width: 100%;
} }
.floating-note { .floating-note {
align-items: center; align-items: center;
background: var(--signal); animation: noteBob 4.2s ease-in-out infinite;
background: linear-gradient(135deg, var(--signal), #ff9a62);
border-radius: 8px; border-radius: 8px;
bottom: 46px; bottom: 54px;
box-shadow: var(--shadow); box-shadow: 0 22px 50px rgba(255, 107, 69, 0.26);
color: #fffaf1; color: #fff;
display: flex; display: flex;
font-weight: 900; font-weight: 950;
gap: 10px; gap: 10px;
padding: 14px 16px; padding: 14px 16px;
position: absolute; position: absolute;
right: 4%; right: 2%;
} }
.section { .section {
padding: clamp(52px, 7vw, 92px) clamp(18px, 5vw, 64px); padding: clamp(56px, 7vw, 98px) clamp(18px, 5vw, 72px);
position: relative;
} }
.intro-grid { .intro-grid {
align-items: end; align-items: end;
background: var(--night); background:
color: #fffaf1; linear-gradient(135deg, rgba(66, 217, 255, 0.13), transparent 38%),
#09151e;
border-block: 1px solid var(--line);
display: grid; display: grid;
gap: 32px; gap: 32px;
grid-template-columns: minmax(0, 0.95fr) minmax(280px, 0.8fr); grid-template-columns: minmax(0, 0.95fr) minmax(280px, 0.8fr);
} }
.intro-grid .section-kicker { .intro-grid .section-kicker {
color: #f2b39f; color: var(--mint);
} }
.intro-grid p:last-child { .intro-grid p:last-child {
color: #c8d2d3; color: var(--muted);
font-size: 19px; font-size: 19px;
line-height: 1.6; line-height: 1.65;
} }
.section-heading { .section-heading {
align-items: end; align-items: end;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 28px; margin-bottom: 32px;
}
.projects-section {
background: linear-gradient(180deg, rgba(11, 28, 40, 0.82), rgba(6, 16, 24, 0.94));
} }
.project-grid { .project-grid {
...@@ -299,59 +453,98 @@ h3 { ...@@ -299,59 +453,98 @@ h3 {
.assistant-box, .assistant-box,
.skill-group, .skill-group,
.contact-list { .contact-list {
background: rgba(255, 250, 241, 0.9); background: var(--surface);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 12px 34px rgba(28, 38, 43, 0.08); box-shadow: 0 18px 55px rgba(0, 0, 0, 0.22);
} }
.project-card { .project-card {
padding: clamp(22px, 3vw, 32px); min-height: 330px;
overflow: hidden;
padding: clamp(22px, 3vw, 34px);
position: relative;
transition: border-color 220ms ease, transform 220ms ease, box-shadow 220ms ease;
}
.project-card::before {
background: linear-gradient(135deg, rgba(66, 217, 255, 0.18), transparent 45%);
content: "";
inset: 0;
opacity: 0;
position: absolute;
transition: opacity 220ms ease;
}
.project-card:hover {
border-color: var(--line-strong);
box-shadow: 0 26px 80px rgba(66, 217, 255, 0.13);
transform: translateY(-6px);
}
.project-card:hover::before {
opacity: 1;
}
.project-card > * {
position: relative;
z-index: 1;
}
.project-index {
color: rgba(157, 242, 255, 0.16);
font-family: Georgia, "Times New Roman", serif;
font-size: 106px;
font-weight: 900;
line-height: 0.8;
position: absolute;
right: 24px;
top: 22px;
} }
.project-meta { .project-meta {
color: var(--muted); color: var(--dim);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 13px; font-size: 13px;
font-weight: 900; font-weight: 950;
gap: 10px; gap: 10px;
justify-content: space-between; justify-content: space-between;
margin-bottom: 18px; margin-bottom: 20px;
text-transform: uppercase; text-transform: uppercase;
} }
.project-card p { .project-card p {
color: var(--muted); color: var(--muted);
line-height: 1.58; line-height: 1.62;
} }
.stack-list, .stack-list,
.project-links { .project-links {
display: flex; margin-top: 20px;
flex-wrap: wrap;
gap: 8px;
margin-top: 18px;
} }
.project-links a { .project-links a {
align-items: center; align-items: center;
color: var(--signal); color: var(--signal-soft);
display: inline-flex; display: inline-flex;
font-weight: 900; font-weight: 950;
gap: 4px; gap: 4px;
text-decoration: none; text-decoration: none;
} }
.project-links a:hover {
color: #fff;
}
.skills-section { .skills-section {
background: #f0e3d3; background:
linear-gradient(90deg, rgba(128, 255, 204, 0.08), transparent 40%),
#07111a;
} }
.skill-cloud { .skill-cloud {
display: flex; margin-bottom: 30px;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 28px;
} }
.skill-groups { .skill-groups {
...@@ -361,16 +554,23 @@ h3 { ...@@ -361,16 +554,23 @@ h3 {
} }
.skill-group { .skill-group {
padding: 20px; padding: 22px;
transition: transform 200ms ease, border-color 200ms ease;
}
.skill-group:hover {
border-color: var(--line-strong);
transform: translateY(-4px);
} }
.skill-group h3 { .skill-group h3 {
color: var(--accent-strong);
font-size: 18px; font-size: 18px;
} }
.skill-group ul { .skill-group ul {
color: var(--muted); color: var(--muted);
line-height: 1.8; line-height: 1.85;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
...@@ -378,20 +578,36 @@ h3 { ...@@ -378,20 +578,36 @@ h3 {
.ai-panel { .ai-panel {
align-items: start; align-items: start;
background:
linear-gradient(135deg, rgba(255, 107, 69, 0.10), transparent 35%),
#061018;
display: grid; display: grid;
gap: clamp(24px, 5vw, 72px); gap: clamp(24px, 5vw, 72px);
grid-template-columns: minmax(0, 0.8fr) minmax(320px, 1fr); grid-template-columns: minmax(0, 0.78fr) minmax(330px, 1fr);
} }
.ai-copy p:last-child { .ai-copy p:last-child {
color: var(--muted); color: var(--muted);
font-size: 18px; font-size: 18px;
line-height: 1.6; line-height: 1.64;
margin-top: 18px; margin-top: 18px;
} }
.assistant-box { .assistant-box {
overflow: hidden;
padding: clamp(20px, 4vw, 32px); padding: clamp(20px, 4vw, 32px);
position: relative;
}
.assistant-box::before {
animation: assistantRail 5s linear infinite;
background: linear-gradient(90deg, transparent, rgba(128, 255, 204, 0.34), transparent);
content: "";
height: 1px;
left: -40%;
position: absolute;
top: 0;
width: 80%;
} }
.assistant-header { .assistant-header {
...@@ -399,9 +615,18 @@ h3 { ...@@ -399,9 +615,18 @@ h3 {
color: var(--accent-strong); color: var(--accent-strong);
display: flex; display: flex;
font-size: 18px; font-size: 18px;
font-weight: 900; font-weight: 950;
gap: 10px; gap: 10px;
margin-bottom: 18px; margin-bottom: 20px;
}
.assistant-header i {
animation: blink 1.3s steps(2, end) infinite;
background: var(--mint);
border-radius: 999px;
height: 8px;
margin-left: auto;
width: 8px;
} }
.quick-questions { .quick-questions {
...@@ -412,21 +637,27 @@ h3 { ...@@ -412,21 +637,27 @@ h3 {
} }
.quick-questions button { .quick-questions button {
background: #f0e3d3; background: rgba(66, 217, 255, 0.08);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
color: var(--ink); color: var(--ink);
cursor: pointer; cursor: pointer;
font-size: 13px; font-size: 13px;
font-weight: 800; font-weight: 850;
padding: 8px 10px; padding: 8px 10px;
transition: background 180ms ease, transform 180ms ease;
}
.quick-questions button:hover {
background: rgba(66, 217, 255, 0.16);
transform: translateY(-2px);
} }
.input-label { .input-label {
color: var(--muted); color: var(--dim);
display: block; display: block;
font-size: 13px; font-size: 13px;
font-weight: 900; font-weight: 950;
margin-bottom: 8px; margin-bottom: 8px;
text-transform: uppercase; text-transform: uppercase;
} }
...@@ -434,72 +665,79 @@ h3 { ...@@ -434,72 +665,79 @@ h3 {
.ask-row { .ask-row {
display: grid; display: grid;
gap: 8px; gap: 8px;
grid-template-columns: minmax(0, 1fr) 48px; grid-template-columns: minmax(0, 1fr) 50px;
} }
.ask-row input { .ask-row input {
background: #fffdf8; background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
color: var(--ink); color: var(--ink);
min-height: 48px; min-height: 50px;
padding: 0 12px; padding: 0 12px;
width: 100%; width: 100%;
} }
.ask-row input:focus {
border-color: var(--accent);
outline: none;
}
.ask-row button { .ask-row button {
align-items: center; align-items: center;
background: var(--accent); background: linear-gradient(135deg, var(--accent), var(--mint));
border: 0; border: 0;
border-radius: 8px; border-radius: 8px;
color: #fffaf1; color: #061018;
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
justify-content: center; justify-content: center;
} }
.answer-box { .answer-box {
background: var(--night); animation: answerIn 260ms ease both;
background: #02080d;
border: 1px solid rgba(128, 255, 204, 0.20);
border-radius: 8px; border-radius: 8px;
color: #fffaf1; color: #fff;
margin-top: 16px; margin-top: 16px;
padding: 18px; padding: 18px;
} }
.answer-box span { .answer-box span {
color: #f2b39f; color: var(--mint);
display: block; display: block;
font-size: 13px; font-size: 13px;
font-weight: 900; font-weight: 950;
margin-bottom: 8px; margin-bottom: 8px;
text-transform: uppercase; text-transform: uppercase;
} }
.answer-box p { .answer-box p {
color: #dce4e5; color: #dce7ef;
line-height: 1.55; line-height: 1.58;
margin: 0; margin: 0;
} }
.contact-section { .contact-section {
align-items: start; align-items: start;
background: var(--night); background: #02080d;
color: #fffaf1; border-top: 1px solid var(--line);
display: grid; display: grid;
gap: 32px; gap: 32px;
grid-template-columns: minmax(0, 0.95fr) minmax(320px, 0.8fr); grid-template-columns: minmax(0, 0.95fr) minmax(320px, 0.8fr);
} }
.contact-section .section-kicker { .contact-section .section-kicker {
color: #f2b39f; color: var(--signal-soft);
} }
.contact-list { .contact-list {
background: rgba(255, 250, 241, 0.08); background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 250, 241, 0.15); border-color: rgba(255, 255, 255, 0.12);
display: grid; display: grid;
gap: 10px; gap: 12px;
padding: 20px; padding: 22px;
} }
.contact-list a, .contact-list a,
...@@ -512,20 +750,145 @@ footer span { ...@@ -512,20 +750,145 @@ footer span {
.contact-list a, .contact-list a,
.contact-list span { .contact-list span {
color: #dce4e5; color: #dce7ef;
text-decoration: none; text-decoration: none;
} }
.contact-list a:hover {
color: var(--accent-strong);
}
footer { footer {
align-items: center; align-items: center;
background: #0d1317; background: #02080d;
color: #bac5c7; border-top: 1px solid rgba(255, 255, 255, 0.08);
color: var(--dim);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
font-size: 14px; font-size: 14px;
gap: 12px; gap: 12px;
justify-content: space-between; justify-content: space-between;
padding: 18px clamp(18px, 5vw, 64px); padding: 18px clamp(18px, 5vw, 72px);
}
.reveal {
animation: revealIn 760ms cubic-bezier(0.2, 0.8, 0.2, 1) var(--delay, 0ms) both;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
}
@keyframes titleRise {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes drift {
from {
background-position: 0 0;
}
to {
background-position: 42px 42px;
}
}
@keyframes sweep {
0%,
100% {
opacity: 0.05;
transform: translateX(-12%);
}
50% {
opacity: 0.7;
transform: translateX(12%);
}
}
@keyframes deviceFloat {
0%,
100% {
transform: rotate(2deg) translateY(0);
}
50% {
transform: rotate(-1deg) translateY(-14px);
}
}
@keyframes pulseGlow {
0%,
100% {
opacity: 0.44;
transform: rotate(8deg) scale(0.96);
}
50% {
opacity: 0.76;
transform: rotate(14deg) scale(1.04);
}
}
@keyframes scan {
0% {
top: -30%;
}
55%,
100% {
top: 112%;
}
}
@keyframes noteBob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-9px);
}
}
@keyframes assistantRail {
from {
transform: translateX(0);
}
to {
transform: translateX(180%);
}
}
@keyframes blink {
50% {
opacity: 0.25;
}
}
@keyframes answerIn {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes revealIn {
from {
opacity: 0;
transform: translateY(26px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
@media (max-width: 900px) { @media (max-width: 900px) {
...@@ -548,6 +911,7 @@ footer { ...@@ -548,6 +911,7 @@ footer {
min-height: auto; min-height: auto;
} }
.hero-metrics,
.project-grid, .project-grid,
.skill-groups { .skill-groups {
grid-template-columns: 1fr; grid-template-columns: 1fr;
...@@ -560,6 +924,23 @@ footer { ...@@ -560,6 +924,23 @@ footer {
.floating-note { .floating-note {
bottom: 18px; bottom: 18px;
max-width: calc(100% - 20px);
right: 0; right: 0;
} }
} }
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
scroll-behavior: auto !important;
transition: none !important;
}
.reveal {
animation: none !important;
opacity: 1;
transform: none;
}
}
import { useMemo, useState } from 'react' import { useEffect, useMemo, useState, type CSSProperties } from 'react'
import { import {
ArrowUpRight, ArrowUpRight,
Bot, Bot,
...@@ -72,6 +72,12 @@ const quickQuestions = [ ...@@ -72,6 +72,12 @@ const quickQuestions = [
'How can I contact Hung?', 'How can I contact Hung?',
] ]
const metrics = [
{ label: 'Mobile projects', value: '02' },
{ label: 'Core stack', value: 'RN + Flutter' },
{ label: 'Graduation', value: '2027' },
]
function answerQuestion(question: string) { function answerQuestion(question: string) {
const normalized = question.toLowerCase() const normalized = question.toLowerCase()
...@@ -102,21 +108,42 @@ function answerQuestion(question: string) { ...@@ -102,21 +108,42 @@ function answerQuestion(question: string) {
function App() { function App() {
const [question, setQuestion] = useState(quickQuestions[0]) const [question, setQuestion] = useState(quickQuestions[0])
const [answer, setAnswer] = useState(answerQuestion(quickQuestions[0])) const [answer, setAnswer] = useState(answerQuestion(quickQuestions[0]))
const [answerPulse, setAnswerPulse] = useState(0)
const featuredSkills = useMemo( const featuredSkills = useMemo(
() => skillGroups.flatMap((group) => group.items).slice(0, 9), () => skillGroups.flatMap((group) => group.items).slice(0, 9),
[], [],
) )
useEffect(() => {
const revealItems = document.querySelectorAll<HTMLElement>('.reveal')
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible')
observer.unobserve(entry.target)
}
})
},
{ threshold: 0.18 },
)
revealItems.forEach((item) => observer.observe(item))
return () => observer.disconnect()
}, [])
function handleAsk(nextQuestion = question) { function handleAsk(nextQuestion = question) {
const trimmed = nextQuestion.trim() const trimmed = nextQuestion.trim()
if (!trimmed) return if (!trimmed) return
setQuestion(trimmed) setQuestion(trimmed)
setAnswer(answerQuestion(trimmed)) setAnswer(answerQuestion(trimmed))
setAnswerPulse((current) => current + 1)
} }
return ( return (
<main> <main>
<div className="ambient-grid" aria-hidden="true" />
<nav className="topbar" aria-label="Primary navigation"> <nav className="topbar" aria-label="Primary navigation">
<a className="brand" href="#hero">TQH</a> <a className="brand" href="#hero">TQH</a>
<div className="nav-links"> <div className="nav-links">
...@@ -128,9 +155,13 @@ function App() { ...@@ -128,9 +155,13 @@ function App() {
</nav> </nav>
<section className="hero" id="hero"> <section className="hero" id="hero">
<div className="hero-copy"> <div className="hero-copy reveal">
<p className="eyebrow">Mobile portfolio / React Native intern</p> <p className="eyebrow">Mobile portfolio / React Native intern</p>
<h1>{profile.name}</h1> <h1>
<span>Tran</span>
<span>Quang</span>
<span>Hung</span>
</h1>
<p className="role">{profile.role}</p> <p className="role">{profile.role}</p>
<p className="summary">{profile.summary}</p> <p className="summary">{profile.summary}</p>
...@@ -149,11 +180,22 @@ function App() { ...@@ -149,11 +180,22 @@ function App() {
<span>Firebase</span> <span>Firebase</span>
<span>Figma UI/UX</span> <span>Figma UI/UX</span>
</div> </div>
<div className="hero-metrics" aria-label="Profile metrics">
{metrics.map((metric) => (
<div key={metric.label}>
<strong>{metric.value}</strong>
<span>{metric.label}</span>
</div>
))}
</div>
</div> </div>
<div className="hero-visual" aria-label="CV preview"> <div className="hero-visual reveal" aria-label="CV preview">
<div className="device-halo" aria-hidden="true" />
<div className="phone-frame"> <div className="phone-frame">
<div className="phone-speaker" /> <div className="phone-speaker" />
<div className="screen-scan" aria-hidden="true" />
<img src="/cv-preview.png" alt="Tran Quang Hung CV preview" /> <img src="/cv-preview.png" alt="Tran Quang Hung CV preview" />
</div> </div>
<div className="floating-note"> <div className="floating-note">
...@@ -163,7 +205,7 @@ function App() { ...@@ -163,7 +205,7 @@ function App() {
</div> </div>
</section> </section>
<section className="section intro-grid" aria-label="Profile summary"> <section className="section intro-grid reveal" aria-label="Profile summary">
<div> <div>
<p className="section-kicker">Internship fit</p> <p className="section-kicker">Internship fit</p>
<h2>Building mobile interfaces with product-minded UI decisions.</h2> <h2>Building mobile interfaces with product-minded UI decisions.</h2>
...@@ -175,15 +217,16 @@ function App() { ...@@ -175,15 +217,16 @@ function App() {
</p> </p>
</section> </section>
<section className="section" id="projects"> <section className="section projects-section" id="projects">
<div className="section-heading"> <div className="section-heading reveal">
<p className="section-kicker">Selected work</p> <p className="section-kicker">Selected work</p>
<h2>Projects from my CV</h2> <h2>Projects from my CV</h2>
</div> </div>
<div className="project-grid"> <div className="project-grid">
{projects.map((project) => ( {projects.map((project, index) => (
<article className="project-card" key={project.name}> <article className="project-card reveal" key={project.name} style={{ '--delay': `${index * 110}ms` } as CSSProperties}>
<div className="project-index">0{index + 1}</div>
<div className="project-meta"> <div className="project-meta">
<span>{project.type}</span> <span>{project.type}</span>
<span>{project.time}</span> <span>{project.time}</span>
...@@ -208,7 +251,7 @@ function App() { ...@@ -208,7 +251,7 @@ function App() {
</section> </section>
<section className="section skills-section" id="skills"> <section className="section skills-section" id="skills">
<div className="section-heading"> <div className="section-heading reveal">
<p className="section-kicker">Capability map</p> <p className="section-kicker">Capability map</p>
<h2>Technical skills</h2> <h2>Technical skills</h2>
</div> </div>
...@@ -220,8 +263,8 @@ function App() { ...@@ -220,8 +263,8 @@ function App() {
</div> </div>
<div className="skill-groups"> <div className="skill-groups">
{skillGroups.map((group) => ( {skillGroups.map((group, index) => (
<div className="skill-group" key={group.title}> <div className="skill-group reveal" key={group.title} style={{ '--delay': `${index * 80}ms` } as CSSProperties}>
<h3>{group.title}</h3> <h3>{group.title}</h3>
<ul> <ul>
{group.items.map((item) => ( {group.items.map((item) => (
...@@ -234,7 +277,7 @@ function App() { ...@@ -234,7 +277,7 @@ function App() {
</section> </section>
<section className="section ai-panel" id="ai"> <section className="section ai-panel" id="ai">
<div className="ai-copy"> <div className="ai-copy reveal">
<p className="section-kicker">AI applied</p> <p className="section-kicker">AI applied</p>
<h2>Profile assistant</h2> <h2>Profile assistant</h2>
<p> <p>
...@@ -243,10 +286,11 @@ function App() { ...@@ -243,10 +286,11 @@ function App() {
</p> </p>
</div> </div>
<div className="assistant-box"> <div className="assistant-box reveal">
<div className="assistant-header"> <div className="assistant-header">
<Bot size={22} /> <Bot size={22} />
<span>Ask about Hung</span> <span>Ask about Hung</span>
<i aria-hidden="true" />
</div> </div>
<div className="quick-questions"> <div className="quick-questions">
...@@ -272,14 +316,14 @@ function App() { ...@@ -272,14 +316,14 @@ function App() {
</button> </button>
</div> </div>
<div className="answer-box"> <div className="answer-box" key={answerPulse}>
<span>Answer</span> <span>Answer</span>
<p>{answer}</p> <p>{answer}</p>
</div> </div>
</div> </div>
</section> </section>
<section className="section contact-section" id="contact"> <section className="section contact-section reveal" id="contact">
<div> <div>
<p className="section-kicker">Contact</p> <p className="section-kicker">Contact</p>
<h2>Ready for mobile app internship work.</h2> <h2>Ready for mobile app internship work.</h2>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment