Compare commits
10 Commits
5927ee40e2
...
463e72a404
| Author | SHA1 | Date | |
|---|---|---|---|
| 463e72a404 | |||
| e45a2fc14a | |||
| ab2683c476 | |||
| caa7ced772 | |||
| 9f14e9a4f4 | |||
| c7ef6dff3e | |||
| a1cab65d1e | |||
| 7c7604299c | |||
| c997acb0e0 | |||
| 2b188aa293 |
15
.dockerignore
Normal file
15
.dockerignore
Normal file
@@ -0,0 +1,15 @@
|
||||
.git
|
||||
.gitignore
|
||||
.next
|
||||
out
|
||||
node_modules
|
||||
|
||||
.env
|
||||
.env.*
|
||||
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
Dockerfile*
|
||||
compose*.yaml
|
||||
compose*.yml
|
||||
23
DEPLOYMENT.md
Normal file
23
DEPLOYMENT.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Deployment
|
||||
|
||||
This project builds the Next.js static export in a Node container and serves the
|
||||
generated files from Nginx on port 80. The container joins the existing
|
||||
`caddy_proxy` Docker network under the `static-site` service name expected by
|
||||
Caddy.
|
||||
|
||||
## Deploy or update
|
||||
|
||||
From the project directory on the server, run:
|
||||
|
||||
```sh
|
||||
docker compose up --build -d
|
||||
```
|
||||
|
||||
The external `caddy_proxy` network must already exist and Caddy must be attached
|
||||
to it. No host port is published because Caddy is the public entry point and
|
||||
terminates TLS for `amiirkhl.ir` and `www.amiirkhl.ir`.
|
||||
|
||||
If the previous `static-site` container was created from a different Compose
|
||||
project directory, remove that old stack once before the first deployment to
|
||||
avoid a Docker container-name conflict. Updates from this project then use only
|
||||
the command above.
|
||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine AS runtime
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/out/ /usr/share/nginx/html/
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/ || exit 1
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
15
compose.yaml
Normal file
15
compose.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
static-site:
|
||||
image: amiirkhl-portfolio:latest
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: static-site
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- caddy_proxy
|
||||
|
||||
networks:
|
||||
caddy_proxy:
|
||||
external: true
|
||||
name: caddy_proxy
|
||||
41
nginx.conf
Normal file
41
nginx.conf
Normal file
@@ -0,0 +1,41 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
charset utf-8;
|
||||
server_tokens off;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types
|
||||
application/javascript
|
||||
application/json
|
||||
application/xml
|
||||
image/svg+xml
|
||||
text/css
|
||||
text/plain;
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
|
||||
location ^~ /_next/static/ {
|
||||
try_files $uri =404;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable";
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri.html $uri/ =404;
|
||||
}
|
||||
|
||||
error_page 404 /404.html;
|
||||
|
||||
location = /404.html {
|
||||
internal;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<svg width="139" height="139" viewBox="0 0 139 139" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M108.635 69.2868C108.635 91.0182 91.0183 108.635 69.287 108.635C47.5556 108.635 29.9388 91.0182 29.9388 69.2868C29.9388 47.5555 47.5556 29.9387 69.287 29.9387C91.0183 29.9387 108.635 47.5555 108.635 69.2868Z" fill="#FEDB4B"/>
|
||||
<rect x="64.1548" width="10.2647" height="22.2402" fill="#FEDB4B"/>
|
||||
<rect x="64.1548" y="116.333" width="10.2647" height="22.2402" fill="#FEDB4B"/>
|
||||
<rect x="0.00012207" y="74.4192" width="10.2647" height="22.2402" transform="rotate(-90 0.00012207 74.4192)" fill="#FEDB4B"/>
|
||||
<rect x="116.334" y="74.4192" width="10.2647" height="22.2402" transform="rotate(-90 116.334 74.4192)" fill="#FEDB4B"/>
|
||||
<rect x="108.375" y="126.725" width="10.2647" height="22.2402" transform="rotate(150 108.375 126.725)" fill="#FEDB4B"/>
|
||||
<rect x="50.2089" y="25.9771" width="10.2647" height="22.2402" transform="rotate(150 50.2089 25.9771)" fill="#FEDB4B"/>
|
||||
<rect x="99.4855" y="6.71606" width="10.2647" height="22.2402" transform="rotate(30 99.4855 6.71606)" fill="#FEDB4B"/>
|
||||
<rect x="41.3184" y="107.464" width="10.2647" height="22.2402" transform="rotate(30 41.3184 107.464)" fill="#FEDB4B"/>
|
||||
<rect x="6.71667" y="39.0881" width="10.2647" height="22.2402" transform="rotate(-60 6.71667 39.0881)" fill="#FEDB4B"/>
|
||||
<rect x="107.464" y="97.2551" width="10.2647" height="22.2402" transform="rotate(-60 107.464 97.2551)" fill="#FEDB4B"/>
|
||||
<rect x="11.849" y="108.375" width="10.2647" height="22.2402" transform="rotate(-120 11.849 108.375)" fill="#FEDB4B"/>
|
||||
<rect x="112.597" y="50.208" width="10.2647" height="22.2402" transform="rotate(-120 112.597 50.208)" fill="#FEDB4B"/>
|
||||
<path d="M108.635 69.2868C108.635 91.0182 91.0183 108.635 69.287 108.635C47.5556 108.635 29.9388 91.0182 29.9388 69.2868C29.9388 47.5555 47.5556 29.9387 69.287 29.9387C91.0183 29.9387 108.635 47.5555 108.635 69.2868Z" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="64.1548" width="10.2647" height="22.2402" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="64.1548" y="116.333" width="10.2647" height="22.2402" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="0.00012207" y="74.4192" width="10.2647" height="22.2402" transform="rotate(-90 0.00012207 74.4192)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="116.334" y="74.4192" width="10.2647" height="22.2402" transform="rotate(-90 116.334 74.4192)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="108.375" y="126.725" width="10.2647" height="22.2402" transform="rotate(150 108.375 126.725)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="50.2089" y="25.9771" width="10.2647" height="22.2402" transform="rotate(150 50.2089 25.9771)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="99.4855" y="6.71606" width="10.2647" height="22.2402" transform="rotate(30 99.4855 6.71606)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="41.3184" y="107.464" width="10.2647" height="22.2402" transform="rotate(30 41.3184 107.464)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="6.71667" y="39.0881" width="10.2647" height="22.2402" transform="rotate(-60 6.71667 39.0881)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="107.464" y="97.2551" width="10.2647" height="22.2402" transform="rotate(-60 107.464 97.2551)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="11.849" y="108.375" width="10.2647" height="22.2402" transform="rotate(-120 11.849 108.375)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
<rect x="112.597" y="50.208" width="10.2647" height="22.2402" transform="rotate(-120 112.597 50.208)" fill="var(--hero-illus-sun, #FFE66D)"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,8 +1,8 @@
|
||||
.educationSection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 60px;
|
||||
padding-bottom: 32px;
|
||||
padding-top: 3.75rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
@@ -10,17 +10,17 @@
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px 32px;
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 36px;
|
||||
gap: 2.25rem;
|
||||
transition:
|
||||
border-color var(--motion-duration-fast) ease,
|
||||
transform var(--motion-duration-default) var(--motion-ease-out);
|
||||
}
|
||||
|
||||
.crestCol {
|
||||
width: 264px;
|
||||
width: 16.5rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -28,8 +28,8 @@
|
||||
}
|
||||
|
||||
.crestContainer {
|
||||
width: 264px;
|
||||
height: 192px;
|
||||
width: 16.5rem;
|
||||
height: 12rem;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -48,7 +48,7 @@
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.date {
|
||||
@@ -58,7 +58,7 @@
|
||||
letter-spacing: var(--letter-spacing-eyebrow);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.detailsCol {
|
||||
@@ -73,7 +73,7 @@
|
||||
line-height: var(--line-height-h3);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -100,16 +100,16 @@
|
||||
|
||||
.crestCol,
|
||||
.crestContainer {
|
||||
width: 180px;
|
||||
height: 132px;
|
||||
width: 11.25rem;
|
||||
height: 8.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.crestCol,
|
||||
.crestContainer {
|
||||
width: 144px;
|
||||
height: 108px;
|
||||
width: 9rem;
|
||||
height: 6.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
.experienceSection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 60px;
|
||||
padding-bottom: 80px;
|
||||
padding-top: 3.75rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
margin-top: 20px;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.timelineItem {
|
||||
display: flex;
|
||||
position: relative;
|
||||
min-height: 180px;
|
||||
min-height: 11.25rem;
|
||||
}
|
||||
|
||||
.spineColumn {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
width: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -28,11 +28,11 @@
|
||||
}
|
||||
|
||||
.nodeDot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-slate-400);
|
||||
margin-top: 6px;
|
||||
margin-top: 0.375rem;
|
||||
z-index: 2;
|
||||
transition:
|
||||
background-color var(--motion-duration-fast) ease,
|
||||
@@ -46,8 +46,8 @@
|
||||
|
||||
.verticalLine {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 14px;
|
||||
top: 1.25rem;
|
||||
bottom: 0.875rem;
|
||||
width: 2px;
|
||||
background-color: var(--border-strong);
|
||||
z-index: 1;
|
||||
@@ -56,21 +56,21 @@
|
||||
.itemContent {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
padding-left: 20px;
|
||||
padding-bottom: 36px;
|
||||
gap: 36px;
|
||||
padding-left: 1.25rem;
|
||||
padding-bottom: 2.25rem;
|
||||
gap: 2.25rem;
|
||||
}
|
||||
|
||||
.identityCol {
|
||||
width: 360px;
|
||||
width: 22.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.metaRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.875rem;
|
||||
}
|
||||
|
||||
.dateBadge {
|
||||
@@ -85,7 +85,7 @@
|
||||
.locationWrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 0.25rem;
|
||||
font-size: var(--font-size-label-small);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@@ -102,12 +102,12 @@
|
||||
.roleIdentityRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
width: 4.5rem;
|
||||
height: 4.5rem;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
@@ -127,7 +127,7 @@
|
||||
.roleTitles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 4px;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.roleTitle {
|
||||
@@ -147,36 +147,36 @@
|
||||
|
||||
.detailsCol {
|
||||
flex-grow: 1;
|
||||
max-width: 820px;
|
||||
padding-top: 4px;
|
||||
max-width: 51.25rem;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-size: var(--font-size-body-small);
|
||||
line-height: var(--line-height-body-small);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.bulletList {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.bulletItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.bulletDot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
width: 0.3125rem;
|
||||
height: 0.3125rem;
|
||||
border-radius: 50%;
|
||||
background-color: var(--text-muted);
|
||||
margin-top: 10px;
|
||||
margin-top: 0.625rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -209,9 +209,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 650px) {
|
||||
.spineColumn {
|
||||
width: 24px;
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.itemContent {
|
||||
@@ -223,12 +223,12 @@
|
||||
}
|
||||
|
||||
.logoContainer {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
width: 3.75rem;
|
||||
height: 3.75rem;
|
||||
}
|
||||
|
||||
.roleTitle {
|
||||
font-size: 18px;
|
||||
line-height: 26px;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.footerSection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 1440px;
|
||||
height: 480px;
|
||||
max-width: var(--layout-frame);
|
||||
height: 30rem;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -16,7 +16,7 @@
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 480px;
|
||||
height: 30rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -32,14 +32,14 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin-top: 84px;
|
||||
margin-top: 5.25rem;
|
||||
}
|
||||
|
||||
.ctaTitle {
|
||||
font-size: var(--font-size-display-footer);
|
||||
line-height: var(--line-height-display-footer);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 0.75rem;
|
||||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@@ -49,20 +49,20 @@
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
letter-spacing: var(--letter-spacing-eyebrow);
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.socialRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 48px;
|
||||
gap: 3rem;
|
||||
}
|
||||
|
||||
.socialItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
transition:
|
||||
@@ -80,8 +80,8 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.socialIcon svg {
|
||||
@@ -105,7 +105,7 @@
|
||||
@media (max-width: 900px) {
|
||||
.footerSection {
|
||||
height: auto;
|
||||
min-height: 480px;
|
||||
min-height: 30rem;
|
||||
padding: var(--space-10) var(--layout-page-gutter) var(--space-9);
|
||||
}
|
||||
|
||||
@@ -127,17 +127,17 @@
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.footerSection {
|
||||
min-height: 560px;
|
||||
min-height: 35rem;
|
||||
padding-top: var(--space-9);
|
||||
}
|
||||
|
||||
.ctaTitle {
|
||||
font-size: 36px;
|
||||
line-height: 46px;
|
||||
font-size: 2.25rem;
|
||||
line-height: 2.875rem;
|
||||
}
|
||||
|
||||
.ctaSubtitle {
|
||||
max-width: 300px;
|
||||
max-width: 18.75rem;
|
||||
margin-bottom: var(--space-7);
|
||||
font-size: var(--font-size-body-default);
|
||||
line-height: var(--line-height-body-default);
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: clamp(18px, 2.5vw, 32px) var(--layout-page-gutter) 0;
|
||||
padding: clamp(1.125rem, 2.5vw, 2rem) var(--layout-page-gutter) 0;
|
||||
}
|
||||
|
||||
/* ── Icon buttons ───────────────────────────────────────────── */
|
||||
@@ -65,25 +65,31 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
/* Horizontal padding keeps text clear of illustration zones */
|
||||
padding: 0 clamp(160px, 22vw, 460px);
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
max-width: 57.5rem;
|
||||
margin: 0 auto;
|
||||
height: auto;
|
||||
min-height: 100%;
|
||||
padding-top: clamp(4rem, 6.5vw, 6rem);
|
||||
padding-bottom: clamp(1.75rem, 3vw, 2.5rem);
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ── Avatar ─────────────────────────────────────────────────── */
|
||||
.avatarWrapper {
|
||||
margin-bottom: clamp(14px, 1.8vw, 22px);
|
||||
margin-bottom: clamp(0.875rem, 1.8vw, 1.375rem);
|
||||
}
|
||||
|
||||
.avatarRingOuter {
|
||||
display: inline-block;
|
||||
padding: clamp(4px, 0.5vw, 7px);
|
||||
padding: clamp(0.25rem, 0.5vw, 0.4375rem);
|
||||
border: clamp(2px, 0.25vw, 3px) solid var(--color-blue-700);
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
box-shadow: 0 8px 32px var(--shadow-color);
|
||||
box-shadow: 0 0.5rem 2rem var(--shadow-color);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .avatarRingOuter {
|
||||
@@ -92,8 +98,8 @@
|
||||
|
||||
.avatarContainer {
|
||||
position: relative;
|
||||
width: clamp(100px, 13vw, 168px);
|
||||
height: clamp(100px, 13vw, 168px);
|
||||
width: clamp(6.25rem, 13vw, 10.5rem);
|
||||
height: clamp(6.25rem, 13vw, 10.5rem);
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
@@ -107,19 +113,19 @@
|
||||
|
||||
/* ── Display title ──────────────────────────────────────────── */
|
||||
.heroTitle {
|
||||
margin-bottom: clamp(8px, 1vw, 14px);
|
||||
margin-bottom: clamp(0.5rem, 1vw, 0.875rem);
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(32px, 5vw, 64px);
|
||||
line-height: 1.12;
|
||||
font-size: clamp(2rem, 3.6vw, 3.375rem);
|
||||
line-height: 1.15;
|
||||
text-shadow: none;
|
||||
/* Allow wrapping on smaller screens so text never clips */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Subtitle ───────────────────────────────────────────────── */
|
||||
.heroSubtitle {
|
||||
margin-bottom: clamp(20px, 2.8vw, 36px);
|
||||
margin-bottom: clamp(1.25rem, 2.8vw, 2.25rem);
|
||||
color: var(--text-primary);
|
||||
font-size: clamp(11px, 1vw, 15px);
|
||||
font-size: clamp(0.6875rem, 1vw, 0.9375rem);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
@@ -129,16 +135,16 @@
|
||||
.actionButtons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: clamp(10px, 1.4vw, 20px);
|
||||
margin-bottom: clamp(18px, 2.2vw, 30px);
|
||||
gap: clamp(0.625rem, 1.4vw, 1.25rem);
|
||||
margin-bottom: clamp(1.125rem, 2.2vw, 1.875rem);
|
||||
}
|
||||
|
||||
.heroButton {
|
||||
min-width: clamp(80px, 9vw, 120px);
|
||||
height: clamp(40px, 4.5vw, 52px);
|
||||
padding: 0 clamp(16px, 2vw, 28px);
|
||||
min-width: clamp(5rem, 9vw, 7.5rem);
|
||||
height: clamp(2.5rem, 4.5vw, 3.25rem);
|
||||
padding: 0 clamp(1rem, 2vw, 1.75rem);
|
||||
border-width: 2px;
|
||||
font-size: clamp(13px, 1.1vw, 16px);
|
||||
font-size: clamp(0.8125rem, 1.1vw, 1rem);
|
||||
}
|
||||
|
||||
.heroButtonOutline {
|
||||
@@ -174,13 +180,15 @@
|
||||
.contactBar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: clamp(10px, 1.4vw, 18px);
|
||||
justify-content: center;
|
||||
gap: clamp(0.625rem, 1.4vw, 1.125rem);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-blue-700);
|
||||
font-size: clamp(11px, 1vw, 14px);
|
||||
font-size: clamp(0.6875rem, 1vw, 0.875rem);
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .contactBar {
|
||||
@@ -190,8 +198,9 @@
|
||||
.contactItem {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 0.375rem;
|
||||
color: inherit;
|
||||
white-space: nowrap;
|
||||
transition: opacity var(--motion-duration-fast) ease;
|
||||
}
|
||||
|
||||
@@ -212,7 +221,7 @@
|
||||
|
||||
.contactDivider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
height: 1.125rem;
|
||||
background: var(--color-blue-500);
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -221,32 +230,34 @@
|
||||
MOBILE ≤ 640px
|
||||
================================================================ */
|
||||
/* ================================================================
|
||||
TABLET 501px – 1100px — match the illustration breakpoint
|
||||
TABLET 651px – 1100px — match the illustration breakpoint
|
||||
================================================================ */
|
||||
@media (max-width: 1100px) and (min-width: 501px) {
|
||||
@media (max-width: 1100px) and (min-width: 651px) {
|
||||
.heroContentWrapper {
|
||||
height: auto;
|
||||
min-height: clamp(540px, 64vw, 640px);
|
||||
min-height: clamp(33.75rem, 64vw, 40rem);
|
||||
}
|
||||
|
||||
.heroContent {
|
||||
height: auto;
|
||||
min-height: clamp(540px, 64vw, 640px);
|
||||
min-height: clamp(33.75rem, 64vw, 40rem);
|
||||
justify-content: flex-start;
|
||||
padding-top: clamp(60px, 8vw, 90px);
|
||||
padding-bottom: 40px;
|
||||
padding-top: clamp(3.75rem, 8vw, 5.625rem);
|
||||
padding-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
/* Allow wrapping at tablet so text stays within the center zone */
|
||||
max-width: 520px;
|
||||
max-width: 32.5rem;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.contactBar {
|
||||
/* Wrap contact items on narrow tablets */
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px 14px;
|
||||
gap: 0.5rem 0.875rem;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.contactDivider {
|
||||
@@ -255,41 +266,41 @@
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
MOBILE ≤ 500px (screensizes under 500px)
|
||||
MOBILE ≤ 650px (screensizes under 650px)
|
||||
================================================================ */
|
||||
@media (max-width: 500px) {
|
||||
@media (max-width: 650px) {
|
||||
.heroContentWrapper {
|
||||
height: auto;
|
||||
min-height: 540px;
|
||||
min-height: 33.75rem;
|
||||
}
|
||||
|
||||
.topBar {
|
||||
padding: 16px 14px 0;
|
||||
padding: 1rem 0.875rem 0;
|
||||
}
|
||||
|
||||
.iconButton {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
}
|
||||
|
||||
.heroContent {
|
||||
height: auto;
|
||||
min-height: 540px;
|
||||
padding: 66px 16px 40px;
|
||||
min-height: 33.75rem;
|
||||
padding: 4.125rem 1rem 2.5rem;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.heroTitle {
|
||||
font-size: clamp(26px, 7.8vw, 36px);
|
||||
font-size: clamp(1.625rem, 7.8vw, 2.25rem);
|
||||
white-space: normal;
|
||||
max-width: 270px;
|
||||
max-width: 17rem;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.heroSubtitle {
|
||||
white-space: normal;
|
||||
max-width: 250px;
|
||||
font-size: 11px;
|
||||
max-width: 15.625rem;
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
@@ -297,23 +308,23 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
max-width: 276px;
|
||||
gap: 6px;
|
||||
max-width: 17.25rem;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.heroButton {
|
||||
min-width: 0;
|
||||
width: auto;
|
||||
height: 38px;
|
||||
padding: 0 6px;
|
||||
font-size: 12px;
|
||||
height: 2.375rem;
|
||||
padding: 0 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.contactBar {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
gap: 0.3125rem;
|
||||
white-space: normal;
|
||||
font-size: 11.5px;
|
||||
font-size: 0.71875rem;
|
||||
}
|
||||
|
||||
.contactItem {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
.milestonesSection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 40px;
|
||||
padding-bottom: 80px;
|
||||
padding-top: 2.5rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.cardsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.certCard {
|
||||
@@ -16,9 +16,9 @@
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 24px 32px;
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
gap: 36px;
|
||||
gap: 2.25rem;
|
||||
transition:
|
||||
border-color var(--motion-duration-fast) ease,
|
||||
transform var(--motion-duration-default) var(--motion-ease-out);
|
||||
@@ -27,8 +27,8 @@
|
||||
|
||||
.thumbnailContainer {
|
||||
position: relative;
|
||||
width: 264px;
|
||||
height: 192px;
|
||||
width: 16.5rem;
|
||||
height: 12rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.certTitle {
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
|
||||
.issuerRow {
|
||||
margin-bottom: 2px;
|
||||
margin-bottom: 0.125rem;
|
||||
}
|
||||
|
||||
.issuerText {
|
||||
@@ -78,14 +78,14 @@
|
||||
letter-spacing: var(--letter-spacing-eyebrow);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--font-size-body-small);
|
||||
line-height: var(--line-height-body-small);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.actionRow {
|
||||
@@ -97,7 +97,7 @@
|
||||
.viewCertLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: 0.375rem;
|
||||
font-size: var(--font-size-body-default);
|
||||
font-weight: 600;
|
||||
color: var(--action-link);
|
||||
@@ -142,6 +142,6 @@
|
||||
}
|
||||
|
||||
.viewCertLink:hover {
|
||||
gap: 10px;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
.projectsSection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 60px;
|
||||
padding-bottom: 80px;
|
||||
padding-top: 3.75rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.headerRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 44px;
|
||||
margin-bottom: 2.75rem;
|
||||
}
|
||||
|
||||
.arrowButtons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 0.75rem;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@
|
||||
overscroll-behavior-inline: contain;
|
||||
touch-action: pan-y;
|
||||
user-select: none;
|
||||
container-type: inline-size;
|
||||
container-name: carousel;
|
||||
}
|
||||
|
||||
.carouselViewport::-webkit-scrollbar {
|
||||
@@ -89,23 +91,28 @@
|
||||
|
||||
.dragging {
|
||||
cursor: grabbing;
|
||||
scroll-snap-type: none !important;
|
||||
scroll-behavior: auto !important;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.carouselTrack {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
width: max-content;
|
||||
gap: 24px;
|
||||
padding: 4px 0 8px;
|
||||
gap: 1.25rem;
|
||||
padding: 0.25rem 0 0.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex: 0 0 var(--size-project-width);
|
||||
flex: 0 0 clamp(18.75rem, 24vw, 21.25rem);
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
width: var(--size-project-width);
|
||||
min-height: 480px;
|
||||
width: clamp(18.75rem, 24vw, 21.25rem);
|
||||
min-height: 28.75rem;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--bg-surface);
|
||||
@@ -116,13 +123,14 @@
|
||||
.mediaContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: var(--size-project-media-height);
|
||||
aspect-ratio: 16 / 10;
|
||||
overflow: hidden;
|
||||
background: var(--color-blue-900);
|
||||
}
|
||||
|
||||
.projectImage {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
pointer-events: none;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
@@ -131,21 +139,21 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
padding: 1.125rem 1rem;
|
||||
}
|
||||
|
||||
.cardTitleRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin-bottom: 6px;
|
||||
gap: 0.4375rem;
|
||||
margin-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-h3);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 700;
|
||||
line-height: var(--line-height-h3);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.titleLinkIcon {
|
||||
@@ -158,9 +166,9 @@
|
||||
}
|
||||
|
||||
.cardDate {
|
||||
margin-bottom: 14px;
|
||||
margin-bottom: 0.625rem;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-size-label-small);
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 700;
|
||||
line-height: var(--line-height-label-small);
|
||||
letter-spacing: var(--letter-spacing-eyebrow);
|
||||
@@ -169,25 +177,28 @@
|
||||
|
||||
.cardDesc {
|
||||
display: -webkit-box;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-body-small);
|
||||
font-size: 0.84375rem;
|
||||
font-weight: 400;
|
||||
line-height: var(--line-height-body-small);
|
||||
line-height: 1.55;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-line-clamp: 3;
|
||||
white-space: normal;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.tagList {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 1.125rem;
|
||||
}
|
||||
|
||||
.tagBadge {
|
||||
padding: 5px 12px;
|
||||
padding: 0.3125rem 0.75rem;
|
||||
border: 0;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-blue-200);
|
||||
@@ -209,7 +220,7 @@
|
||||
.caseStudyLink {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 0.625rem;
|
||||
color: var(--color-blue-500);
|
||||
font-size: var(--font-size-body-small);
|
||||
font-weight: 600;
|
||||
@@ -229,12 +240,14 @@
|
||||
.scrollbarWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 40px;
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
width: 480px;
|
||||
height: 20px;
|
||||
--thumb-width: 13.5rem;
|
||||
width: 30rem;
|
||||
max-width: 100%;
|
||||
height: 1.25rem;
|
||||
margin: 0;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
@@ -242,14 +255,14 @@
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
height: 0.375rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-blue-200);
|
||||
}
|
||||
|
||||
.scrollbar::-webkit-slider-thumb {
|
||||
width: 216px;
|
||||
height: 6px;
|
||||
width: var(--thumb-width, 13.5rem);
|
||||
height: 0.375rem;
|
||||
margin-top: 0;
|
||||
appearance: none;
|
||||
border: 0;
|
||||
@@ -258,15 +271,15 @@
|
||||
}
|
||||
|
||||
.scrollbar::-moz-range-track {
|
||||
height: 6px;
|
||||
height: 0.375rem;
|
||||
border: 0;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-blue-200);
|
||||
}
|
||||
|
||||
.scrollbar::-moz-range-thumb {
|
||||
width: 216px;
|
||||
height: 6px;
|
||||
width: var(--thumb-width, 13.5rem);
|
||||
height: 0.375rem;
|
||||
border: 0;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--color-blue-400);
|
||||
@@ -294,65 +307,192 @@
|
||||
background: var(--color-blue-300);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
/* ================================================================
|
||||
LAPTOP 1101px – 1440px
|
||||
Comfortable ~290px-330px cards (~3 to 4 visible)
|
||||
================================================================ */
|
||||
@media (max-width: 1440px) and (min-width: 1101px) {
|
||||
.carouselTrack {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 0 0 clamp(18.125rem, 23vw, 20.625rem);
|
||||
width: clamp(18.125rem, 23vw, 20.625rem);
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
width: min(30rem, 50vw);
|
||||
--thumb-width: 7.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
TABLET 651px – 1100px
|
||||
Comfortable ~310px-350px cards (~2 cards visible)
|
||||
================================================================ */
|
||||
@media (max-width: 1100px) and (min-width: 651px) {
|
||||
.projectsSection {
|
||||
padding-top: var(--space-8);
|
||||
padding-bottom: var(--space-9);
|
||||
}
|
||||
|
||||
.carouselTrack {
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-basis: min(340px, calc(100vw - (2 * var(--layout-page-gutter))));
|
||||
width: min(340px, calc(100vw - (2 * var(--layout-page-gutter))));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.headerRow {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.arrowButtons {
|
||||
align-self: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.navButton {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.carouselViewport {
|
||||
margin-inline: calc(-1 * var(--layout-page-gutter));
|
||||
width: calc(100% + (2 * var(--layout-page-gutter)));
|
||||
padding-inline: var(--layout-page-gutter);
|
||||
scroll-padding-inline: var(--layout-page-gutter);
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-padding-inline: 0;
|
||||
}
|
||||
|
||||
.carouselTrack {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex-basis: min(320px, calc(100vw - 40px));
|
||||
width: min(320px, calc(100vw - 40px));
|
||||
min-height: 460px;
|
||||
flex: 0 0 clamp(19.375rem, 42vw, 21.875rem);
|
||||
width: clamp(19.375rem, 42vw, 21.875rem);
|
||||
min-height: 28rem;
|
||||
box-sizing: border-box;
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
|
||||
.mediaContainer {
|
||||
height: 200px;
|
||||
aspect-ratio: 16 / 10;
|
||||
}
|
||||
|
||||
.cardBody {
|
||||
padding: var(--space-5);
|
||||
padding: 1.125rem 1rem;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.cardDesc {
|
||||
font-size: 0.84375rem;
|
||||
line-height: 1.55;
|
||||
margin-bottom: 0.875rem;
|
||||
-webkit-line-clamp: 3;
|
||||
white-space: normal;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.tagList {
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tagBadge {
|
||||
padding: 0.25rem 0.5625rem;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.scrollbarWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
width: min(22.5rem, 60vw);
|
||||
--thumb-width: 5.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
MOBILE ≤ 650px
|
||||
Clean 1-column layout: 1 card visible at a time with page gutter
|
||||
================================================================ */
|
||||
@media (max-width: 650px) {
|
||||
.projectsSection {
|
||||
padding-top: 3rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.headerRow {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.arrowButtons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.carouselViewport {
|
||||
width: 100%;
|
||||
margin-inline: 0;
|
||||
padding-inline: 0;
|
||||
scroll-padding-inline: 0;
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.carouselTrack {
|
||||
gap: 1rem;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 0 0 calc(100vw - (2 * var(--layout-page-gutter)));
|
||||
flex: 0 0 100cqw;
|
||||
width: calc(100vw - (2 * var(--layout-page-gutter)));
|
||||
width: 100cqw;
|
||||
min-width: calc(100vw - (2 * var(--layout-page-gutter)));
|
||||
min-width: 100cqw;
|
||||
max-width: 100%;
|
||||
min-height: 26.25rem;
|
||||
box-sizing: border-box;
|
||||
scroll-snap-align: start;
|
||||
scroll-snap-stop: always;
|
||||
}
|
||||
|
||||
.mediaContainer {
|
||||
aspect-ratio: 16 / 10;
|
||||
}
|
||||
|
||||
.cardBody {
|
||||
padding: 1.125rem 1rem;
|
||||
}
|
||||
|
||||
.cardTitle {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.cardDesc {
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.875rem;
|
||||
white-space: normal;
|
||||
word-break: normal;
|
||||
overflow-wrap: break-word;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
|
||||
.tagList {
|
||||
gap: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tagBadge {
|
||||
padding: 0.25rem 0.5625rem;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.scrollbarWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
width: min(12.5rem, 60vw);
|
||||
height: 1rem;
|
||||
--thumb-width: 3.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
|
||||
@@ -12,7 +12,13 @@ export const ProjectsSection: React.FC = () => {
|
||||
active: false,
|
||||
moved: false,
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startScrollLeft: 0,
|
||||
lastX: 0,
|
||||
lastTime: 0,
|
||||
velocity: 0,
|
||||
pointerId: -1,
|
||||
directionLocked: null as 'horizontal' | 'vertical' | null,
|
||||
});
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
@@ -51,7 +57,7 @@ export const ProjectsSection: React.FC = () => {
|
||||
};
|
||||
|
||||
const handlePointerDown = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (event.button !== 0) return;
|
||||
if (event.button !== 0 && event.pointerType === 'mouse') return;
|
||||
|
||||
const viewport = viewportRef.current;
|
||||
if (!viewport) return;
|
||||
@@ -60,30 +66,112 @@ export const ProjectsSection: React.FC = () => {
|
||||
active: true,
|
||||
moved: false,
|
||||
startX: event.clientX,
|
||||
startY: event.clientY,
|
||||
startScrollLeft: viewport.scrollLeft,
|
||||
lastX: event.clientX,
|
||||
lastTime: performance.now(),
|
||||
velocity: 0,
|
||||
pointerId: event.pointerId,
|
||||
directionLocked: null,
|
||||
};
|
||||
viewport.setPointerCapture(event.pointerId);
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
const viewport = viewportRef.current;
|
||||
if (!viewport || !dragState.current.active) return;
|
||||
|
||||
const distance = event.clientX - dragState.current.startX;
|
||||
if (Math.abs(distance) > 3) dragState.current.moved = true;
|
||||
viewport.scrollLeft = dragState.current.startScrollLeft - distance;
|
||||
const deltaX = event.clientX - dragState.current.startX;
|
||||
const deltaY = event.clientY - dragState.current.startY;
|
||||
|
||||
// Detect user intent: horizontal carousel swipe vs vertical page scroll
|
||||
if (!dragState.current.directionLocked) {
|
||||
if (Math.abs(deltaX) < 6 && Math.abs(deltaY) < 6) return;
|
||||
|
||||
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||
// User intends to scroll page vertically, release touch
|
||||
dragState.current.directionLocked = 'vertical';
|
||||
dragState.current.active = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// User intends horizontal carousel drag
|
||||
dragState.current.directionLocked = 'horizontal';
|
||||
try {
|
||||
viewport.setPointerCapture(event.pointerId);
|
||||
} catch {
|
||||
// Ignore if pointer capture unavailable
|
||||
}
|
||||
setIsDragging(true);
|
||||
}
|
||||
|
||||
if (dragState.current.directionLocked === 'horizontal') {
|
||||
const now = performance.now();
|
||||
const dt = Math.max(now - dragState.current.lastTime, 1);
|
||||
const dx = event.clientX - dragState.current.lastX;
|
||||
dragState.current.velocity = dx / dt;
|
||||
dragState.current.lastX = event.clientX;
|
||||
dragState.current.lastTime = now;
|
||||
|
||||
if (Math.abs(deltaX) > 4) dragState.current.moved = true;
|
||||
viewport.scrollLeft = dragState.current.startScrollLeft - deltaX;
|
||||
}
|
||||
};
|
||||
|
||||
const stopDragging = (event: React.PointerEvent<HTMLDivElement>) => {
|
||||
const viewport = viewportRef.current;
|
||||
if (!viewport || !dragState.current.active) return;
|
||||
if (!viewport || !dragState.current.active) {
|
||||
setIsDragging(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const wasHorizontal = dragState.current.directionLocked === 'horizontal';
|
||||
const moved = dragState.current.moved;
|
||||
const velocity = dragState.current.velocity;
|
||||
const clientX = event.clientX ?? dragState.current.lastX;
|
||||
const totalDeltaX = clientX - dragState.current.startX;
|
||||
const pointerId = dragState.current.pointerId;
|
||||
|
||||
dragState.current.active = false;
|
||||
if (viewport.hasPointerCapture(event.pointerId)) {
|
||||
viewport.releasePointerCapture(event.pointerId);
|
||||
if (viewport.hasPointerCapture(pointerId)) {
|
||||
try {
|
||||
viewport.releasePointerCapture(pointerId);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
setIsDragging(false);
|
||||
|
||||
if (!wasHorizontal || !moved) return;
|
||||
|
||||
// Calculate snap target with velocity fling
|
||||
const track = viewport.firstElementChild as HTMLElement | null;
|
||||
const card = track?.firstElementChild as HTMLElement | null;
|
||||
if (!track || !card) return;
|
||||
|
||||
const cardWidth = card.getBoundingClientRect().width;
|
||||
const gap = Number.parseFloat(window.getComputedStyle(track).columnGap) || 0;
|
||||
const step = cardWidth + gap;
|
||||
if (step <= 0) return;
|
||||
|
||||
const currentScroll = viewport.scrollLeft;
|
||||
let targetIndex: number;
|
||||
if (Math.abs(velocity) > 0.2 || Math.abs(totalDeltaX) > cardWidth * 0.18) {
|
||||
if (totalDeltaX < 0 || velocity < -0.2) {
|
||||
targetIndex = Math.ceil(currentScroll / step);
|
||||
} else {
|
||||
targetIndex = Math.floor(currentScroll / step);
|
||||
}
|
||||
} else {
|
||||
targetIndex = Math.round(currentScroll / step);
|
||||
}
|
||||
|
||||
const maxIndex = projects.length - 1;
|
||||
targetIndex = Math.max(0, Math.min(targetIndex, maxIndex));
|
||||
|
||||
viewport.scrollTo({
|
||||
left: targetIndex * step,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickCapture = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -102,6 +190,16 @@ export const ProjectsSection: React.FC = () => {
|
||||
viewport.scrollLeft = (Number(event.target.value) / 100) * maxScroll;
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'ArrowLeft') {
|
||||
event.preventDefault();
|
||||
scrollByCard(-1);
|
||||
} else if (event.key === 'ArrowRight') {
|
||||
event.preventDefault();
|
||||
scrollByCard(1);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="projects"
|
||||
@@ -152,6 +250,7 @@ export const ProjectsSection: React.FC = () => {
|
||||
onPointerUp={stopDragging}
|
||||
onPointerCancel={stopDragging}
|
||||
onClickCapture={handleClickCapture}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={0}
|
||||
aria-label="Scrollable project cards"
|
||||
>
|
||||
@@ -163,6 +262,8 @@ export const ProjectsSection: React.FC = () => {
|
||||
src={project.image}
|
||||
alt={project.title}
|
||||
fill
|
||||
sizes="(max-width: 650px) 100vw, (max-width: 1100px) 100vw, 360px"
|
||||
priority={project.id === 'qlockify'}
|
||||
draggable={false}
|
||||
className={styles.projectImage}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.journeySection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 1050px;
|
||||
height: 65.625rem;
|
||||
overflow: hidden;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
@@ -20,10 +20,10 @@
|
||||
|
||||
.introBlock {
|
||||
position: absolute;
|
||||
top: 112px;
|
||||
top: 7rem;
|
||||
left: var(--layout-page-gutter);
|
||||
z-index: 3;
|
||||
width: min(390px, calc(100% - (2 * var(--layout-page-gutter))));
|
||||
width: min(24.375rem, calc(100% - (2 * var(--layout-page-gutter))));
|
||||
padding: var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--overlay-contact);
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
.underlineBar {
|
||||
width: 120px;
|
||||
width: 7.5rem;
|
||||
height: 3px;
|
||||
margin-bottom: var(--space-5);
|
||||
border-radius: var(--radius-pill);
|
||||
@@ -74,31 +74,31 @@
|
||||
.stepCard {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
width: 250px;
|
||||
width: 15.625rem;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--overlay-contact);
|
||||
box-shadow: 0 8px 24px var(--shadow-color);
|
||||
box-shadow: 0 0.5rem 1.5rem var(--shadow-color);
|
||||
backdrop-filter: blur(8px);
|
||||
transition:
|
||||
border-color var(--motion-duration-fast) ease,
|
||||
transform var(--motion-duration-default) var(--motion-ease-out);
|
||||
}
|
||||
|
||||
.step05 { top: 116px; right: 46px; }
|
||||
.step04 { top: 334px; right: 250px; }
|
||||
.step03 { top: 500px; left: 48%; }
|
||||
.step02 { top: 690px; left: 25%; }
|
||||
.step01 { top: 826px; left: 70px; }
|
||||
.step05 { top: 7.25rem; right: 2.875rem; }
|
||||
.step04 { top: 20.875rem; right: 15.625rem; }
|
||||
.step03 { top: 31.25rem; left: 48%; }
|
||||
.step02 { top: 43.125rem; left: 25%; }
|
||||
.step01 { top: 51.625rem; left: 4.375rem; }
|
||||
|
||||
.stepNumber {
|
||||
flex: 0 0 auto;
|
||||
color: var(--accent-default);
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 800;
|
||||
line-height: 28px;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.stepTitle {
|
||||
@@ -117,24 +117,24 @@
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.introBlock {
|
||||
top: 80px;
|
||||
top: 5rem;
|
||||
}
|
||||
|
||||
.stepCard {
|
||||
width: 230px;
|
||||
width: 14.375rem;
|
||||
}
|
||||
|
||||
.step05 { top: 90px; right: 32px; }
|
||||
.step04 { top: 300px; right: 10%; }
|
||||
.step03 { top: 490px; left: 48%; }
|
||||
.step02 { top: 690px; left: 24%; }
|
||||
.step01 { top: 850px; left: 32px; }
|
||||
.step05 { top: 5.625rem; right: 2rem; }
|
||||
.step04 { top: 18.75rem; right: 10%; }
|
||||
.step03 { top: 30.625rem; left: 48%; }
|
||||
.step02 { top: 43.125rem; left: 24%; }
|
||||
.step01 { top: 53.125rem; left: 2rem; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.journeySection {
|
||||
height: auto;
|
||||
min-height: 1120px;
|
||||
min-height: 70rem;
|
||||
padding: var(--layout-section-padding) var(--layout-page-gutter);
|
||||
}
|
||||
|
||||
@@ -151,13 +151,13 @@
|
||||
padding: var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--overlay-contact);
|
||||
box-shadow: 0 8px 24px var(--shadow-color);
|
||||
box-shadow: 0 0.5rem 1.5rem var(--shadow-color);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.heading {
|
||||
font-size: 28px;
|
||||
line-height: 38px;
|
||||
font-size: 1.75rem;
|
||||
line-height: 2.375rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -171,17 +171,17 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
margin-top: 220px;
|
||||
padding-left: 20px;
|
||||
margin-top: 13.75rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.stepsList::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
left: 5px;
|
||||
width: 1px;
|
||||
top: 1.25rem;
|
||||
bottom: 1.25rem;
|
||||
left: 0.5rem;
|
||||
width: 2px;
|
||||
background: var(--border-strong);
|
||||
}
|
||||
|
||||
@@ -202,19 +202,34 @@
|
||||
.stepCard::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: -20px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
top: 1.375rem;
|
||||
left: -1rem;
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
transform: translateX(-50%);
|
||||
border: 3px solid var(--bg-page);
|
||||
border-radius: 50%;
|
||||
background: var(--accent-default);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1441px) {
|
||||
.journeySection {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.journeySection::after {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translateX(-50%);
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.stepCard:hover {
|
||||
border-color: var(--accent-default);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import { AboutRiverScene } from './scenes/AboutRiverScene';
|
||||
import styles from './RiverJourney.module.css';
|
||||
|
||||
const steps = [
|
||||
{ number: '05', title: 'Product Engineer', description: 'Merging product strategy with engineering to ship impactful products.' },
|
||||
{ number: '04', title: 'Software Engineer', description: 'Engineered scalable applications and delivered real-world solutions.' },
|
||||
{ number: '03', title: 'Computer Science', description: 'Built strong foundations in algorithms and systems thinking.' },
|
||||
{ number: '02', title: 'UI/UX Designer', description: 'Designed intuitive interfaces and user-centered experiences.' },
|
||||
{ number: '01', title: 'Digital Artist', description: 'Explored visual design, illustration and storytelling.' },
|
||||
{ number: '02', title: 'UI/UX Designer', description: 'Designed intuitive interfaces and user-centered experiences.' },
|
||||
{ number: '03', title: 'Computer Science', description: 'Built strong foundations in algorithms and systems thinking.' },
|
||||
{ number: '04', title: 'Software Engineer', description: 'Engineered scalable applications and delivered real-world solutions.' },
|
||||
{ number: '05', title: 'Product Engineer', description: 'Merging product strategy with engineering to ship impactful products.' },
|
||||
];
|
||||
|
||||
export const RiverJourney: React.FC = () => {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: clamp(520px, 46vw, 680px);
|
||||
min-height: clamp(36.25rem, 48vw, 43.75rem);
|
||||
overflow: hidden;
|
||||
background: var(--bg-page);
|
||||
}
|
||||
@@ -35,17 +35,58 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* ── All backdrop imgs are absolutely positioned ─────────────── */
|
||||
/* ── All backdrop SVGs are absolutely positioned ─────────────── */
|
||||
.heroBackdrop svg,
|
||||
.heroBackdrop img {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* ── Dark-mode tint ─────────────────────────────────────────── */
|
||||
.heroBackdrop svg path,
|
||||
.heroBackdrop svg rect {
|
||||
transition: fill 0.3s ease;
|
||||
}
|
||||
|
||||
/* ── Dark-mode styling ─────────────────────────────────────────── */
|
||||
:global([data-theme="dark"]) .heroBackdrop {
|
||||
opacity: 0.82;
|
||||
filter: saturate(0.88) brightness(0.9);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── Celestial Bodies (Sun & Moon) ──────────────────────────── */
|
||||
.backdropSun,
|
||||
.backdropMoon {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: auto;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
transition: opacity 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||
transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Light mode (default): Sun shines brightly with solar glow, Moon hidden */
|
||||
.backdropSun {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
filter: drop-shadow(0 0 16px var(--hero-illus-sun-glow)) drop-shadow(0 0 4px rgba(255, 255, 255, 0.85));
|
||||
}
|
||||
|
||||
.backdropMoon {
|
||||
opacity: 0;
|
||||
transform: scale(0.65) rotate(-25deg);
|
||||
}
|
||||
|
||||
/* Dark mode: Bright half-moon shines with lunar glow, Sun gracefully sets */
|
||||
:global([data-theme="dark"]) .backdropSun {
|
||||
opacity: 0;
|
||||
transform: scale(0.65) rotate(35deg);
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) .backdropMoon {
|
||||
opacity: 1;
|
||||
transform: scale(1) rotate(0deg);
|
||||
filter: drop-shadow(0 0 16px var(--hero-illus-moon-glow)) drop-shadow(0 0 4px rgba(255, 255, 255, 0.8));
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
@@ -64,21 +105,22 @@
|
||||
width: clamp(250px, 29vw, 460px);
|
||||
}
|
||||
|
||||
.backdropSun {
|
||||
.backdropSun,
|
||||
.backdropMoon {
|
||||
top: clamp(20px, 3.5vw, 52px);
|
||||
right: clamp(100px, calc(22% + 2vw), 380px);
|
||||
width: clamp(52px, 10vw, 139px);
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
TABLET 501px – 1100px
|
||||
TABLET 651px – 1100px
|
||||
Large illustrations with heavy bleed — bold fragments at edges.
|
||||
Width scales fluidly; fixed bleed offset keeps content clear.
|
||||
================================================================ */
|
||||
@media (max-width: 1100px) and (min-width: 501px) {
|
||||
@media (max-width: 1100px) and (min-width: 651px) {
|
||||
.heroStage {
|
||||
height: auto;
|
||||
min-height: clamp(540px, 64vw, 640px);
|
||||
min-height: clamp(33.75rem, 64vw, 40rem);
|
||||
}
|
||||
|
||||
/* Left — visible slice ≈ width - 150px, always < content padding */
|
||||
@@ -95,8 +137,9 @@
|
||||
width: clamp(295px, 38vw, 380px);
|
||||
}
|
||||
|
||||
/* Sun — upper-right zone, between avatar and right cubes */
|
||||
.backdropSun {
|
||||
/* Celestial — upper-right zone, between avatar and right cubes */
|
||||
.backdropSun,
|
||||
.backdropMoon {
|
||||
top: clamp(26px, 3.5vw, 44px);
|
||||
right: clamp(120px, 33vw, 280px);
|
||||
width: clamp(62px, 9vw, 96px);
|
||||
@@ -104,33 +147,34 @@
|
||||
}
|
||||
|
||||
/* ================================================================
|
||||
MOBILE ≤ 500px (screensizes under 500px)
|
||||
MOBILE ≤ 650px (screensizes under 650px)
|
||||
Assets moved UP to the sides (matching desktop/tablet aesthetic):
|
||||
- Anchored via top, flanking the avatar & title
|
||||
- Fluid side bleed maintains balance between visibility & clearance
|
||||
- Zero collision with central avatar, title, buttons, or contact bar
|
||||
================================================================ */
|
||||
@media (max-width: 500px) {
|
||||
@media (max-width: 650px) {
|
||||
.heroStage {
|
||||
height: auto;
|
||||
min-height: 540px;
|
||||
min-height: 33.75rem;
|
||||
}
|
||||
|
||||
.backdropLeft {
|
||||
top: clamp(75px, 16vw, 95px);
|
||||
bottom: auto;
|
||||
left: clamp(-140px, calc(20vw - 230px), -100px);
|
||||
left: clamp(-100px, calc(20vw - 230px), -100px);
|
||||
width: clamp(200px, 44vw, 250px);
|
||||
}
|
||||
|
||||
.backdropRight {
|
||||
top: clamp(82px, 18vw, 102px);
|
||||
bottom: auto;
|
||||
right: clamp(-150px, calc(20vw - 240px), -120px);
|
||||
right: clamp(-130px, calc(20vw - 240px), -120px);
|
||||
width: clamp(210px, 46vw, 260px);
|
||||
}
|
||||
|
||||
.backdropSun {
|
||||
.backdropSun,
|
||||
.backdropMoon {
|
||||
top: clamp(24px, 5vw, 36px);
|
||||
right: clamp(52px, 15vw, 68px);
|
||||
width: clamp(34px, 8vw, 42px);
|
||||
@@ -146,7 +190,8 @@
|
||||
================================================================ */
|
||||
@media (min-width: 1441px) {
|
||||
.heroStage {
|
||||
height: 662px;
|
||||
height: auto;
|
||||
min-height: 42.5rem;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -162,7 +207,8 @@
|
||||
width: 418px;
|
||||
}
|
||||
|
||||
.backdropSun {
|
||||
.backdropSun,
|
||||
.backdropMoon {
|
||||
top: 50px;
|
||||
right: 346px;
|
||||
width: 139px;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.skillsSection {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 60px;
|
||||
padding-bottom: 80px;
|
||||
padding-top: 3.75rem;
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
|
||||
.mainCard {
|
||||
@@ -10,9 +10,9 @@
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-xl);
|
||||
padding: 36px 40px;
|
||||
box-shadow: 0 4px 20px var(--shadow-color);
|
||||
margin-bottom: 32px;
|
||||
padding: 2.25rem 2.5rem;
|
||||
box-shadow: 0 0.25rem 1.25rem var(--shadow-color);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.cardEyebrow {
|
||||
@@ -23,25 +23,25 @@
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.skillsBlock {
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.skillsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
row-gap: 28px;
|
||||
column-gap: 40px;
|
||||
margin-bottom: 32px;
|
||||
row-gap: 1.75rem;
|
||||
column-gap: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.skillItem {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.skillName {
|
||||
@@ -53,13 +53,13 @@
|
||||
|
||||
.meterSegments {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
gap: 0.375rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.segment {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
height: 0.375rem;
|
||||
border-radius: 3px;
|
||||
background-color: var(--border-default);
|
||||
transition: background-color 0.2s ease;
|
||||
@@ -72,11 +72,11 @@
|
||||
.badgeCloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.badgePill {
|
||||
padding: 6px 16px;
|
||||
padding: 0.375rem 1rem;
|
||||
border-radius: var(--radius-pill);
|
||||
background-color: var(--bg-subtle);
|
||||
border: 1px solid var(--border-default);
|
||||
@@ -90,13 +90,14 @@
|
||||
}
|
||||
|
||||
.toolsBlock {
|
||||
padding-top: 16px;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.toolsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.toolTile {
|
||||
@@ -104,12 +105,16 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 132px;
|
||||
height: 136px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
min-height: 6.5rem;
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px 8px;
|
||||
padding: 0.75rem 0.375rem;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
opacity var(--motion-duration-default) ease,
|
||||
filter var(--motion-duration-default) ease,
|
||||
@@ -125,12 +130,12 @@
|
||||
}
|
||||
|
||||
.toolIconContainer {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
width: clamp(2.25rem, 42%, 3.5rem);
|
||||
height: clamp(2.25rem, 42%, 3.5rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.toolIcon {
|
||||
@@ -140,21 +145,25 @@
|
||||
}
|
||||
|
||||
.toolLabel {
|
||||
font-size: var(--font-size-body-small);
|
||||
line-height: var(--line-height-body-small);
|
||||
font-size: clamp(0.71875rem, 1vw, 0.8125rem);
|
||||
line-height: 1.25;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filterTabsRow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
min-width: 180px;
|
||||
min-width: 11.25rem;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .mainCard {
|
||||
@@ -168,15 +177,11 @@
|
||||
@media (max-width: 1180px) {
|
||||
.toolsGrid {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.toolTile {
|
||||
width: 100%;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 850px) {
|
||||
.skillsSection {
|
||||
padding-top: var(--space-8);
|
||||
padding-bottom: var(--space-9);
|
||||
@@ -193,6 +198,7 @@
|
||||
|
||||
.toolsGrid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.filterTabsRow {
|
||||
@@ -200,7 +206,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
@media (max-width: 600px) {
|
||||
.mainCard {
|
||||
padding: var(--space-5);
|
||||
}
|
||||
@@ -211,18 +217,8 @@
|
||||
}
|
||||
|
||||
.toolsGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.toolTile {
|
||||
height: 124px;
|
||||
padding: var(--space-3) var(--space-2);
|
||||
}
|
||||
|
||||
.toolIconContainer {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.625rem;
|
||||
}
|
||||
|
||||
.filterTabsRow {
|
||||
@@ -236,6 +232,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.mainCard {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.toolsGrid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.badgePill:hover {
|
||||
border-color: var(--accent-default);
|
||||
|
||||
@@ -12,16 +12,15 @@ import {
|
||||
} from './scenePrimitives';
|
||||
import styles from './Scenes.module.css';
|
||||
|
||||
// Sharp 90-degree orthogonal isometric river path:
|
||||
// Enters from bottom-left along the isometric Z axis,
|
||||
// takes a sharp 90-degree orthogonal step across the isometric X axis at the center,
|
||||
// and turns 90 degrees to continue along the isometric Z axis towards the top-right.
|
||||
const riverPath = [
|
||||
new THREE.Vector2(4.5, -13),
|
||||
new THREE.Vector2(2.1, -7.4),
|
||||
new THREE.Vector2(0.2, -3.8),
|
||||
new THREE.Vector2(0.2, -1.2),
|
||||
new THREE.Vector2(1.5, 0.2),
|
||||
new THREE.Vector2(-0.8, 2.4),
|
||||
new THREE.Vector2(-0.1, 4.1),
|
||||
new THREE.Vector2(-2.5, 8.1),
|
||||
new THREE.Vector2(-3.3, 13),
|
||||
new THREE.Vector2(-1.3, -16),
|
||||
new THREE.Vector2(-1.3, 0),
|
||||
new THREE.Vector2(1.3, 0),
|
||||
new THREE.Vector2(1.3, 16),
|
||||
];
|
||||
|
||||
const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||
@@ -42,7 +41,7 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||
});
|
||||
|
||||
const ground = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(34, 34),
|
||||
new THREE.PlaneGeometry(60, 60),
|
||||
new THREE.MeshStandardMaterial({ color: palette.ground, roughness: 1 }),
|
||||
);
|
||||
ground.rotation.x = -Math.PI / 2;
|
||||
@@ -51,9 +50,9 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||
world.add(ground);
|
||||
|
||||
const gridVertices: number[] = [];
|
||||
for (let value = -17; value <= 17; value += 1) {
|
||||
gridVertices.push(-17, -0.3, value, 17, -0.3, value);
|
||||
gridVertices.push(value, -0.3, -17, value, -0.3, 17);
|
||||
for (let value = -30; value <= 30; value += 1) {
|
||||
gridVertices.push(-30, -0.3, value, 30, -0.3, value);
|
||||
gridVertices.push(value, -0.3, -30, value, -0.3, 30);
|
||||
}
|
||||
const gridGeometry = new THREE.BufferGeometry();
|
||||
gridGeometry.setAttribute('position', new THREE.Float32BufferAttribute(gridVertices, 3));
|
||||
@@ -96,8 +95,8 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||
world.add(primaryPlates, secondaryPlates, tertiaryPlates);
|
||||
|
||||
[
|
||||
[-7.5, -7.7, 9.5, 5.1], [-6.2, -2.7, 8.5, 4.2], [-6.5, 2.2, 8.7, 4.3],
|
||||
[-7.1, 7.2, 9.2, 5.0], [7.4, -8.0, 9.4, 5.4], [6.2, -2.9, 8.0, 4.1],
|
||||
[-7.5, -7.7, 9.5, 5.1], [-6.6, -2.7, 8.5, 4.2], [-6.8, 2.2, 8.7, 4.3],
|
||||
[-7.1, 7.2, 9.2, 5.0], [7.4, -8.0, 9.4, 5.4], [6.4, -2.9, 8.0, 4.1],
|
||||
[6.6, 2.0, 8.4, 4.2], [6.3, 7.0, 9.1, 5.0],
|
||||
].forEach(([x, z, width, depth]) => addPlate(primaryPlates, x, z, width, depth));
|
||||
|
||||
@@ -108,7 +107,7 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||
addPlate(secondaryPlates, x, z, width, depth, 0.5, theme === 'dark' ? 0x0b2f5f : 0xe6f0fe),
|
||||
);
|
||||
|
||||
const riverGeometry = new THREE.ShapeGeometry(createRibbonShape(riverPath, 2.2));
|
||||
const riverGeometry = new THREE.ShapeGeometry(createRibbonShape(riverPath, 1.8));
|
||||
const waterMaterial = new THREE.MeshStandardMaterial({
|
||||
color: palette.water,
|
||||
emissive: palette.water,
|
||||
@@ -130,15 +129,15 @@ const createAboutScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||
const direction = end.clone().sub(start);
|
||||
const length = direction.length();
|
||||
direction.normalize();
|
||||
for (let distance = 1; distance < length; distance += 1.15) {
|
||||
for (let distance = 1.3; distance < length - 0.7; distance += 1.35) {
|
||||
const at = start.clone().addScaledVector(direction, distance);
|
||||
const seam = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(2.08, 0.014),
|
||||
new THREE.MeshBasicMaterial({ color: palette.edge, transparent: true, opacity: 0.35 }),
|
||||
new THREE.PlaneGeometry(1.72, 0.016),
|
||||
new THREE.MeshBasicMaterial({ color: palette.edge, transparent: true, opacity: 0.32 }),
|
||||
);
|
||||
seam.rotation.x = -Math.PI / 2;
|
||||
seam.rotation.z = Math.atan2(direction.y, direction.x) + Math.PI / 2;
|
||||
seam.position.set(at.x, -0.2, at.y);
|
||||
seam.position.set(at.x, -0.21, -at.y);
|
||||
tertiaryPlates.add(seam);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { useTheme } from '@/context/ThemeContext';
|
||||
import { ThreeCanvas, type SceneFactory } from './ThreeCanvas';
|
||||
import {
|
||||
addSoftLights,
|
||||
createMesa,
|
||||
createTree,
|
||||
fitOrthographicCamera,
|
||||
palettes,
|
||||
} from './scenePrimitives';
|
||||
import styles from './Scenes.module.css';
|
||||
|
||||
const createHeroScene: SceneFactory = ({ pointer, theme, reduceMotion }) => {
|
||||
const palette = palettes[theme];
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.OrthographicCamera(-12, 12, 6, -6, 0.1, 50);
|
||||
const cameraBase = new THREE.Vector3(0, 8.5, 17);
|
||||
camera.position.copy(cameraBase);
|
||||
camera.lookAt(0, 2.2, 0);
|
||||
addSoftLights(scene, theme, 15);
|
||||
|
||||
const world = new THREE.Group();
|
||||
scene.add(world);
|
||||
|
||||
const ground = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(30, 18),
|
||||
new THREE.MeshStandardMaterial({ color: palette.ground, roughness: 1 }),
|
||||
);
|
||||
ground.rotation.x = -Math.PI / 2;
|
||||
ground.position.set(0, -0.03, 1);
|
||||
ground.receiveShadow = true;
|
||||
world.add(ground);
|
||||
|
||||
const clearing = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(7.6, 4.6),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: palette.water,
|
||||
roughness: 0.72,
|
||||
transparent: true,
|
||||
opacity: theme === 'dark' ? 0.26 : 0.18,
|
||||
}),
|
||||
);
|
||||
clearing.rotation.x = -Math.PI / 2;
|
||||
clearing.position.set(0, 0.015, 1.2);
|
||||
world.add(clearing);
|
||||
|
||||
const secondaryMountains = new THREE.Group();
|
||||
const mobileDecor = new THREE.Group();
|
||||
world.add(secondaryMountains, mobileDecor);
|
||||
const mountainData = [
|
||||
[-11.3, -1.7, 4.3, 4.5, 4.0, 0], [-7.8, -2.7, 4.0, 4.1, 4.0, 0],
|
||||
[-4.8, -3.6, 2.4, 2.7, 2.5, 1], [-11.5, 3.3, 3.1, 3.3, 2.6, 1],
|
||||
[-8.8, 4.2, 3.2, 3.0, 2.5, 1], [10.2, -2.1, 4.7, 4.7, 4.3, 0],
|
||||
[7.5, -3.1, 3.8, 3.9, 3.8, 0], [4.8, -3.8, 2.3, 2.5, 2.4, 1],
|
||||
[10.8, 3.6, 3.8, 3.6, 3.1, 1], [8.1, 4.3, 3.0, 3.1, 2.5, 1],
|
||||
] as const;
|
||||
|
||||
mountainData.forEach(([x, z, width, height, depth, secondary]) => {
|
||||
const mesa = createMesa(width, height, depth, palette);
|
||||
mesa.position.set(x, 0, z);
|
||||
(secondary ? secondaryMountains : world).add(mesa);
|
||||
});
|
||||
|
||||
const mobileLeftMesa = createMesa(2.2, 2.6, 2.3, palette);
|
||||
mobileLeftMesa.position.set(-3.1, 0, -0.6);
|
||||
const mobileRightMesa = createMesa(2.2, 2.8, 2.3, palette);
|
||||
mobileRightMesa.position.set(3.1, 0, -0.8);
|
||||
const mobileLeftTree = createTree(palette, 0.72);
|
||||
mobileLeftTree.position.set(-2.1, 0, 2.2);
|
||||
const mobileRightTree = createTree(palette, 0.68);
|
||||
mobileRightTree.position.set(2.1, 0, 2.4);
|
||||
mobileDecor.add(mobileLeftMesa, mobileRightMesa, mobileLeftTree, mobileRightTree);
|
||||
mobileDecor.visible = false;
|
||||
|
||||
const secondaryTrees = new THREE.Group();
|
||||
world.add(secondaryTrees);
|
||||
const treeData = [
|
||||
[-5.6, -0.3, 1, 0], [-4.6, 0.2, 0.85, 0], [-3.8, -1.4, 0.72, 1],
|
||||
[-8.2, 2.0, 0.78, 1], [-7.1, 3.0, 1.0, 0], [-5.8, 3.5, 0.72, 0],
|
||||
[5.7, -0.4, 1.0, 0], [4.7, 0.2, 0.82, 0], [3.9, -1.5, 0.7, 1],
|
||||
[8.4, 2.2, 0.82, 1], [7.2, 3.2, 1.0, 0], [5.9, 3.8, 0.72, 0],
|
||||
] as const;
|
||||
const animatedTrees: THREE.Group[] = [];
|
||||
treeData.forEach(([x, z, scale, secondary]) => {
|
||||
const tree = createTree(palette, scale, secondary === 1);
|
||||
tree.position.set(x, 0, z);
|
||||
(secondary ? secondaryTrees : world).add(tree);
|
||||
animatedTrees.push(tree);
|
||||
});
|
||||
|
||||
const celestial = new THREE.Group();
|
||||
let celestialBaseY = 6.25;
|
||||
celestial.position.set(-5.8, 6.25, -1.5);
|
||||
celestial.lookAt(camera.position);
|
||||
scene.add(celestial);
|
||||
|
||||
if (theme === 'light') {
|
||||
const sun = new THREE.Mesh(
|
||||
new THREE.CircleGeometry(0.62, 32),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffd75a, toneMapped: false }),
|
||||
);
|
||||
celestial.add(sun);
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
const angle = (index / 12) * Math.PI * 2;
|
||||
const ray = new THREE.Mesh(
|
||||
new THREE.PlaneGeometry(0.13, 0.42),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffd75a, toneMapped: false }),
|
||||
);
|
||||
ray.position.set(Math.sin(angle) * 1.05, Math.cos(angle) * 1.05, 0);
|
||||
ray.rotation.z = -angle;
|
||||
celestial.add(ray);
|
||||
}
|
||||
} else {
|
||||
const moon = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(0.66, 24, 16),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: palette.snow,
|
||||
emissive: palette.accent,
|
||||
emissiveIntensity: 0.22,
|
||||
roughness: 0.8,
|
||||
}),
|
||||
);
|
||||
celestial.add(moon);
|
||||
const moonLight = new THREE.PointLight(palette.accent, 7, 16, 2);
|
||||
celestial.add(moonLight);
|
||||
}
|
||||
|
||||
return {
|
||||
scene,
|
||||
camera,
|
||||
resize: (width, height) => {
|
||||
const mobile = width < 640;
|
||||
const tablet = width < 1024;
|
||||
secondaryMountains.visible = !mobile;
|
||||
secondaryTrees.visible = !mobile;
|
||||
mobileDecor.visible = mobile;
|
||||
world.scale.setScalar(mobile ? 0.9 : tablet ? 0.96 : 1);
|
||||
celestial.position.x = mobile ? -2.35 : tablet ? -4.1 : -5.8;
|
||||
celestialBaseY = mobile ? 6.8 : 6.25;
|
||||
celestial.position.y = celestialBaseY;
|
||||
fitOrthographicCamera(camera, width, height, mobile ? 14.5 : tablet ? 12.5 : 11.5);
|
||||
},
|
||||
update: (elapsed) => {
|
||||
celestial.position.y = celestialBaseY + Math.sin(elapsed * 0.55) * 0.08;
|
||||
celestial.rotation.z = theme === 'light' ? elapsed * 0.025 : 0;
|
||||
animatedTrees.forEach((tree, index) => {
|
||||
tree.rotation.z = Math.sin(elapsed * 0.7 + index) * 0.006;
|
||||
});
|
||||
if (!reduceMotion) {
|
||||
camera.position.x += (cameraBase.x + pointer.x * 0.2 - camera.position.x) * 0.035;
|
||||
camera.position.y += (cameraBase.y + pointer.y * 0.1 - camera.position.y) * 0.035;
|
||||
camera.lookAt(0, 2.2, 0);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function HeroLandscapeScene() {
|
||||
const { theme } = useTheme();
|
||||
return <ThreeCanvas className={styles.heroScene} createScene={createHeroScene} theme={theme} />;
|
||||
}
|
||||
@@ -9,16 +9,20 @@
|
||||
|
||||
.heroScene {
|
||||
z-index: 0;
|
||||
background:
|
||||
linear-gradient(to bottom, #c7edfb 0%, #dff5fc 47%, #f8faff 82%, var(--bg-page) 100%);
|
||||
}
|
||||
|
||||
.heroScene::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom, transparent 82%, var(--bg-page) 100%);
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) .heroScene {
|
||||
background:
|
||||
radial-gradient(ellipse 48% 58% at 50% 45%, var(--bg-page) 0 44%, transparent 82%),
|
||||
linear-gradient(to bottom, transparent 68%, var(--bg-page) 100%);
|
||||
opacity: 0.76;
|
||||
linear-gradient(to bottom, #071936 0%, #06142f 50%, #020e28 82%, var(--bg-page) 100%);
|
||||
}
|
||||
|
||||
.heroScene canvas,
|
||||
@@ -46,10 +50,28 @@
|
||||
|
||||
.aboutScene {
|
||||
bottom: auto;
|
||||
height: 640px;
|
||||
height: 40rem;
|
||||
}
|
||||
|
||||
.aboutScene canvas {
|
||||
opacity: 0.68;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1441px) {
|
||||
.aboutScene {
|
||||
overflow: visible;
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translateX(-50%);
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.aboutScene::after {
|
||||
left: 50%;
|
||||
right: auto;
|
||||
transform: translateX(-50%);
|
||||
width: 100vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,19 +60,52 @@ export function fitOrthographicCamera(
|
||||
|
||||
export function createRibbonShape(points: THREE.Vector2[], width: number) {
|
||||
const halfWidth = width / 2;
|
||||
const offsetPoint = (index: number, direction: 1 | -1) => {
|
||||
const current = points[index].clone();
|
||||
const previous = points[Math.max(0, index - 1)];
|
||||
const next = points[Math.min(points.length - 1, index + 1)];
|
||||
const tangent = next.clone().sub(previous).normalize();
|
||||
return current.add(new THREE.Vector2(-tangent.y, tangent.x).multiplyScalar(halfWidth * direction));
|
||||
};
|
||||
const n = points.length;
|
||||
if (n < 2) return new THREE.Shape();
|
||||
|
||||
const left: THREE.Vector2[] = [];
|
||||
const right: THREE.Vector2[] = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
let normal: THREE.Vector2;
|
||||
let miterScale = 1;
|
||||
|
||||
if (i === 0) {
|
||||
const d = points[1].clone().sub(points[0]).normalize();
|
||||
normal = new THREE.Vector2(-d.y, d.x);
|
||||
} else if (i === n - 1) {
|
||||
const d = points[n - 1].clone().sub(points[n - 2]).normalize();
|
||||
normal = new THREE.Vector2(-d.y, d.x);
|
||||
} else {
|
||||
const d1 = points[i].clone().sub(points[i - 1]).normalize();
|
||||
const d2 = points[i + 1].clone().sub(points[i]).normalize();
|
||||
const n1 = new THREE.Vector2(-d1.y, d1.x);
|
||||
const n2 = new THREE.Vector2(-d2.y, d2.x);
|
||||
const miter = n1.clone().add(n2);
|
||||
const len = miter.length();
|
||||
if (len > 0.0001) {
|
||||
miter.normalize();
|
||||
const dot = Math.max(miter.dot(n1), 0.25);
|
||||
miterScale = Math.min(1 / dot, 2.5);
|
||||
normal = miter;
|
||||
} else {
|
||||
normal = n1;
|
||||
}
|
||||
}
|
||||
|
||||
const offset = normal.clone().multiplyScalar(halfWidth * miterScale);
|
||||
left.push(points[i].clone().add(offset));
|
||||
right.push(points[i].clone().sub(offset));
|
||||
}
|
||||
|
||||
const left = points.map((_, index) => offsetPoint(index, 1));
|
||||
const right = points.map((_, index) => offsetPoint(index, -1)).reverse();
|
||||
const shape = new THREE.Shape();
|
||||
shape.moveTo(left[0].x, left[0].y);
|
||||
[...left.slice(1), ...right].forEach((point) => shape.lineTo(point.x, point.y));
|
||||
for (let i = 1; i < left.length; i++) {
|
||||
shape.lineTo(left[i].x, left[i].y);
|
||||
}
|
||||
for (let i = right.length - 1; i >= 0; i--) {
|
||||
shape.lineTo(right[i].x, right[i].y);
|
||||
}
|
||||
shape.closePath();
|
||||
return shape;
|
||||
}
|
||||
@@ -92,7 +125,7 @@ export function createMesa(
|
||||
halfWidth - inset, height, halfDepth - inset, -halfWidth + inset, height, halfDepth - inset,
|
||||
];
|
||||
const indices = [
|
||||
4, 5, 6, 4, 6, 7,
|
||||
4, 6, 5, 4, 7, 6,
|
||||
0, 1, 5, 0, 5, 4,
|
||||
1, 2, 6, 1, 6, 5,
|
||||
2, 3, 7, 2, 7, 6,
|
||||
@@ -101,15 +134,33 @@ export function createMesa(
|
||||
const geometry = new THREE.BufferGeometry();
|
||||
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
geometry.setIndex(indices);
|
||||
geometry.clearGroups();
|
||||
geometry.addGroup(0, 6, 0);
|
||||
geometry.addGroup(6, 6, 1);
|
||||
geometry.addGroup(12, 6, 2);
|
||||
geometry.addGroup(18, 6, 1);
|
||||
geometry.addGroup(24, 6, 2);
|
||||
geometry.computeVertexNormals();
|
||||
|
||||
const mesa = new THREE.Mesh(
|
||||
geometry,
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: palette.mountain,
|
||||
flatShading: true,
|
||||
roughness: 0.9,
|
||||
}),
|
||||
[
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: palette.mountainLight,
|
||||
flatShading: true,
|
||||
roughness: 0.9,
|
||||
}),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: palette.mountain,
|
||||
flatShading: true,
|
||||
roughness: 0.92,
|
||||
}),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: palette.mountainDark,
|
||||
flatShading: true,
|
||||
roughness: 0.94,
|
||||
}),
|
||||
],
|
||||
);
|
||||
mesa.castShadow = true;
|
||||
mesa.receiveShadow = true;
|
||||
|
||||
@@ -16,10 +16,23 @@
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 100%;
|
||||
scroll-behavior: smooth;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
html {
|
||||
font-size: 93.75%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
html {
|
||||
font-size: 87.5%;
|
||||
}
|
||||
}
|
||||
|
||||
html[data-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -48,7 +61,15 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 1320px Inner Content Container with 60px Gutters */
|
||||
/* On ultra-wide viewports let decorative bleed elements escape the
|
||||
canvas. body { overflow-x: hidden } prevents horizontal scrollbars. */
|
||||
@media (min-width: 1441px) {
|
||||
.desktop-canvas {
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/* 1320px Inner Content Container with Page Gutters */
|
||||
.content-container {
|
||||
width: calc(100% - (2 * var(--layout-page-gutter)));
|
||||
max-width: var(--layout-container);
|
||||
@@ -56,18 +77,18 @@ body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 650px) {
|
||||
.section-header {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28px;
|
||||
line-height: 38px;
|
||||
font-size: 1.75rem;
|
||||
line-height: 2.375rem;
|
||||
}
|
||||
|
||||
.section-underline {
|
||||
width: 88px;
|
||||
width: 5.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +97,7 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 40px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.section-eyebrow {
|
||||
@@ -86,7 +107,7 @@ body {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section-title-wrap {
|
||||
@@ -100,11 +121,11 @@ body {
|
||||
line-height: var(--line-height-h2);
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.section-underline {
|
||||
width: 120px;
|
||||
width: 7.5rem;
|
||||
height: 3px;
|
||||
background-color: var(--accent-default);
|
||||
border-radius: var(--radius-pill);
|
||||
@@ -121,9 +142,9 @@ body {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 0.5rem;
|
||||
height: var(--size-button-height);
|
||||
padding: 0 24px;
|
||||
padding: 0 1.5rem;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: var(--font-size-button);
|
||||
line-height: var(--line-height-button);
|
||||
|
||||
@@ -21,65 +21,65 @@
|
||||
--color-border-strong-light: #A9BEDA;
|
||||
--color-border-strong-dark: #426B9D;
|
||||
--color-black: #000000;
|
||||
--space-0: 0px;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 24px;
|
||||
--space-6: 32px;
|
||||
--space-7: 40px;
|
||||
--space-8: 48px;
|
||||
--space-9: 64px;
|
||||
--space-10: 80px;
|
||||
--space-11: 96px;
|
||||
--space-12: 120px;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--radius-xl: 24px;
|
||||
--radius-pill: 999px;
|
||||
--space-0: 0;
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 0.75rem;
|
||||
--space-4: 1rem;
|
||||
--space-5: 1.5rem;
|
||||
--space-6: 2rem;
|
||||
--space-7: 2.5rem;
|
||||
--space-8: 3rem;
|
||||
--space-9: 4rem;
|
||||
--space-10: 5rem;
|
||||
--space-11: 6rem;
|
||||
--space-12: 7.5rem;
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-md: 0.75rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.5rem;
|
||||
--radius-pill: 9999px;
|
||||
--stroke-default: 1px;
|
||||
--stroke-strong: 2px;
|
||||
--layout-frame: 1440px;
|
||||
--layout-container: 1320px;
|
||||
--layout-page-gutter: 60px;
|
||||
--layout-grid-gutter: 24px;
|
||||
--layout-grid-column: 88px;
|
||||
--layout-section-gap: 120px;
|
||||
--layout-section-padding: 96px;
|
||||
--layout-text-measure: 720px;
|
||||
--size-icon-sm: 16px;
|
||||
--size-icon-md: 20px;
|
||||
--size-icon-lg: 24px;
|
||||
--size-icon-button: 44px;
|
||||
--size-button-height: 48px;
|
||||
--size-social-button: 56px;
|
||||
--size-avatar: 200px;
|
||||
--size-journey-image: 96px;
|
||||
--size-project-width: 360px;
|
||||
--size-project-media-height: 225px;
|
||||
--size-tool-tile: 72px;
|
||||
--font-size-display-hero: 64px;
|
||||
--font-size-display-footer: 48px;
|
||||
--font-size-h1: 40px;
|
||||
--font-size-h2: 32px;
|
||||
--font-size-h3: 20px;
|
||||
--font-size-body-large: 18px;
|
||||
--font-size-body-default: 16px;
|
||||
--font-size-body-small: 14px;
|
||||
--font-size-label-small: 12px;
|
||||
--font-size-button: 15px;
|
||||
--line-height-display-hero: 72px;
|
||||
--line-height-display-footer: 56px;
|
||||
--line-height-h1: 52px;
|
||||
--line-height-h2: 42px;
|
||||
--line-height-h3: 28px;
|
||||
--line-height-body-large: 30px;
|
||||
--line-height-body-default: 26px;
|
||||
--line-height-body-small: 22px;
|
||||
--line-height-label-small: 18px;
|
||||
--line-height-button: 20px;
|
||||
--layout-page-gutter: 3.75rem;
|
||||
--layout-grid-gutter: 1.5rem;
|
||||
--layout-grid-column: 5.5rem;
|
||||
--layout-section-gap: 7.5rem;
|
||||
--layout-section-padding: 6rem;
|
||||
--layout-text-measure: 45rem;
|
||||
--size-icon-sm: 1rem;
|
||||
--size-icon-md: 1.25rem;
|
||||
--size-icon-lg: 1.5rem;
|
||||
--size-icon-button: 2.75rem;
|
||||
--size-button-height: 3rem;
|
||||
--size-social-button: 3.5rem;
|
||||
--size-avatar: 12.5rem;
|
||||
--size-journey-image: 6rem;
|
||||
--size-project-width: 22.5rem;
|
||||
--size-project-media-height: 14.0625rem;
|
||||
--size-tool-tile: 4.5rem;
|
||||
--font-size-display-hero: 4rem;
|
||||
--font-size-display-footer: 3rem;
|
||||
--font-size-h1: 2.5rem;
|
||||
--font-size-h2: 2rem;
|
||||
--font-size-h3: 1.25rem;
|
||||
--font-size-body-large: 1.125rem;
|
||||
--font-size-body-default: 1rem;
|
||||
--font-size-body-small: 0.875rem;
|
||||
--font-size-label-small: 0.75rem;
|
||||
--font-size-button: 0.9375rem;
|
||||
--line-height-display-hero: 4.5rem;
|
||||
--line-height-display-footer: 3.5rem;
|
||||
--line-height-h1: 3.25rem;
|
||||
--line-height-h2: 2.625rem;
|
||||
--line-height-h3: 1.75rem;
|
||||
--line-height-body-large: 1.875rem;
|
||||
--line-height-body-default: 1.625rem;
|
||||
--line-height-body-small: 1.375rem;
|
||||
--line-height-label-small: 1.125rem;
|
||||
--line-height-button: 1.25rem;
|
||||
--letter-spacing-eyebrow: 0.16em;
|
||||
--motion-duration-fast: 140ms;
|
||||
--motion-duration-default: 200ms;
|
||||
@@ -107,6 +107,19 @@
|
||||
--focus-ring: #3D8AFA;
|
||||
--overlay-contact: rgba(255, 255, 255, 0.721569);
|
||||
--shadow-color: rgba(16, 63, 127, 0.078431);
|
||||
|
||||
/* Hero Scenic Landscape Palette (Day / Light Mode) */
|
||||
--hero-illus-cube-top: #9DCDFD;
|
||||
--hero-illus-cube-front: #2066AB;
|
||||
--hero-illus-cube-side: #0E4073;
|
||||
--hero-illus-cube-shadow: #0C234F;
|
||||
--hero-illus-tree-light: #2166A9;
|
||||
--hero-illus-tree-dark: #104179;
|
||||
--hero-illus-tree-trunk: #08234B;
|
||||
--hero-illus-sun: #FFE66D;
|
||||
--hero-illus-sun-glow: rgba(255, 220, 80, 0.6);
|
||||
--hero-illus-moon: #FFFDF0;
|
||||
--hero-illus-moon-glow: rgba(255, 253, 240, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@@ -131,18 +144,31 @@
|
||||
--focus-ring: #82B5FF;
|
||||
--overlay-contact: rgba(1, 13, 39, 0.721569);
|
||||
--shadow-color: rgba(0, 0, 0, 0.239216);
|
||||
|
||||
/* Hero Scenic Landscape Palette (Night / Dark Mode) */
|
||||
--hero-illus-cube-top: #528AE6;
|
||||
--hero-illus-cube-front: #1D4685;
|
||||
--hero-illus-cube-side: #122955;
|
||||
--hero-illus-cube-shadow: #091A3E;
|
||||
--hero-illus-tree-light: #428CE8;
|
||||
--hero-illus-tree-dark: #18366B;
|
||||
--hero-illus-tree-trunk: #0A1B3D;
|
||||
--hero-illus-sun: #FDE047;
|
||||
--hero-illus-sun-glow: rgba(253, 224, 71, 0.45);
|
||||
--hero-illus-moon: #FFFDF0;
|
||||
--hero-illus-moon-glow: rgba(255, 253, 240, 0.65);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
:root {
|
||||
--layout-page-gutter: 32px;
|
||||
--layout-section-padding: 80px;
|
||||
--layout-page-gutter: 2rem;
|
||||
--layout-section-padding: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@media (max-width: 650px) {
|
||||
:root {
|
||||
--layout-page-gutter: 20px;
|
||||
--layout-section-padding: 64px;
|
||||
--layout-page-gutter: 1.25rem;
|
||||
--layout-section-padding: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user