diff options
Diffstat (limited to 'themes')
20 files changed, 2989 insertions, 0 deletions
diff --git a/themes/danixme/assets/css/main.css b/themes/danixme/assets/css/main.css new file mode 100644 index 0000000..649adae --- /dev/null +++ b/themes/danixme/assets/css/main.css @@ -0,0 +1,1750 @@ +/* ── Reset & Variables ──────────────────────────── */ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } + +:root { + --bg: #060b10; + --bg2: #0c1520; + --surface: #101e2d; + --border: #182840; + --accent: #a855f7; + --accent2: #00ff88; + --accent-glow: rgba(168, 85, 247, 0.12); + --text: #c4d6e8; + --text-dim: #7a9bb8; + --muted: #304860; + --font-mono: 'JetBrains Mono', 'Courier New', monospace; + --font-head: 'Oxanium', sans-serif; +} + +html { scroll-behavior: smooth; font-size: 17px; overflow-x: hidden; } + +body { + background-color: var(--bg); + color: var(--text); + font-family: var(--font-mono); + font-size: 1rem; + line-height: 1.8; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + overflow-wrap: break-word; +} + +/* Dot grid */ +body::before { + content: ''; + position: fixed; + inset: 0; + background-image: radial-gradient(circle, rgba(168,85,247,0.07) 1px, transparent 1px); + background-size: 30px 30px; + pointer-events: none; + z-index: 0; +} + +/* ── Light Theme ────────────────────────────────── */ +/* Class applied by inline script before CSS renders — no flash. */ +/* Media query block is a no-JS fallback only. */ + +html.theme-light { + --bg: #f0f4f8; + --bg2: #e2eaf4; + --surface: #d4dff0; + --border: #a8bdd8; + --accent: #7c3aed; + --accent2: #008f5a; + --accent-glow: rgba(124, 58, 237, 0.1); + --text: #0d1b2a; + --text-dim: #2e4a6a; + --muted: #6888a8; +} +html.theme-light body::before { + background-image: radial-gradient(circle, rgba(124,58,237,0.07) 1px, transparent 1px); +} +html.theme-light nav { + background: rgba(240, 244, 248, 0.92); + border-bottom-color: rgba(168, 189, 216, 0.6); +} +html.theme-light #matrix-canvas { opacity: 0.18; } +html.theme-light #hero::after { + background: radial-gradient(ellipse, rgba(124,58,237,0.06) 0%, transparent 65%); +} +html.theme-light .hero-name { + text-shadow: 0 0 80px rgba(124,58,237,0.12), 0 0 120px rgba(124,58,237,0.05); +} +html.theme-light .hero-term { + background: rgba(10, 20, 35, 0.95); + border-color: rgba(124, 58, 237, 0.35); + box-shadow: 0 0 40px rgba(124,58,237,0.08), inset 0 0 30px rgba(0,0,0,0.5); + /* Reset colour variables to dark values so text is legible on dark bg */ + --text: #c4d6e8; + --text-dim: #7a9bb8; + --muted: #304860; + --accent: #a855f7; + --accent2: #00ff88; + /* Re-apply color so it resolves against the local --text above */ + color: var(--text); +} +html.theme-light #scroll-progress { + box-shadow: 0 0 8px rgba(124,58,237,0.45); +} + +/* No-JS fallback via media query */ +@media (prefers-color-scheme: light) { + :root { + --bg: #f0f4f8; + --bg2: #e2eaf4; + --surface: #d4dff0; + --border: #a8bdd8; + --accent: #7c3aed; + --accent2: #008f5a; + --accent-glow: rgba(124, 58, 237, 0.1); + --text: #0d1b2a; + --text-dim: #2e4a6a; + --muted: #6888a8; + } + body::before { + background-image: radial-gradient(circle, rgba(124,58,237,0.07) 1px, transparent 1px); + } + nav { background: rgba(240,244,248,0.92); border-bottom-color: rgba(168,189,216,0.6); } + #matrix-canvas { opacity: 0.18; } + #hero::after { background: radial-gradient(ellipse, rgba(124,58,237,0.06) 0%, transparent 65%); } + .hero-name { text-shadow: 0 0 80px rgba(124,58,237,0.12), 0 0 120px rgba(124,58,237,0.05); } + .hero-term { background: rgba(10,20,35,0.95); border-color: rgba(124,58,237,0.35); box-shadow: 0 0 40px rgba(124,58,237,0.08), inset 0 0 30px rgba(0,0,0,0.5); --text: #c4d6e8; --text-dim: #7a9bb8; --muted: #304860; --accent: #a855f7; --accent2: #00ff88; color: var(--text); } + #scroll-progress { box-shadow: 0 0 8px rgba(124,58,237,0.45); } +} + +/* Force dark even when system preference is light */ +html.theme-dark { + --bg: #060b10; + --bg2: #0c1520; + --surface: #101e2d; + --border: #182840; + --accent: #a855f7; + --accent2: #00ff88; + --accent-glow: rgba(168, 85, 247, 0.12); + --text: #c4d6e8; + --text-dim: #7a9bb8; + --muted: #304860; +} +html.theme-dark nav { + background: rgba(6,11,16,0.88); + border-bottom-color: rgba(24,40,64,0.5); +} +html.theme-dark body::before { + background-image: radial-gradient(circle, rgba(168,85,247,0.07) 1px, transparent 1px); +} +html.theme-dark #matrix-canvas { opacity: 0.13; } +html.theme-dark #hero::after { + background: radial-gradient(ellipse, rgba(168,85,247,0.07) 0%, transparent 65%); +} +html.theme-dark .hero-name { + text-shadow: 0 0 80px rgba(168,85,247,0.18), 0 0 120px rgba(168,85,247,0.08); +} +html.theme-dark .hero-term { + background: rgba(6, 11, 16, 0.85); + border-color: rgba(168,85,247,0.3); + box-shadow: 0 0 40px rgba(168,85,247,0.1), inset 0 0 30px rgba(0,0,0,0.4); +} +html.theme-dark #scroll-progress { + box-shadow: 0 0 8px rgba(168,85,247,0.6); +} + +/* ── Container ──────────────────────────────────── */ +.container { + max-width: 1080px; + margin: 0 auto; + padding: 0 1.2rem; + position: relative; + z-index: 1; +} + +@media (min-width: 768px) { + .container { padding: 0 2rem; } +} + +/* ── Scroll Progress ─────────────────────────────── */ +#scroll-progress { + position: fixed; + top: 0; + left: 0; + height: 2px; + width: 0%; + background: linear-gradient(to right, var(--accent), var(--accent2)); + box-shadow: 0 0 8px rgba(168,85,247,0.6); + z-index: 9999; + pointer-events: none; +} + +/* ── Navbar ─────────────────────────────────────── */ +nav { + position: fixed; + inset-block-start: 0; + inset-inline: 0; + z-index: 100; + padding: 1rem 1.2rem; + display: flex; + align-items: center; + justify-content: space-between; + background: rgba(6,11,16,0.88); + backdrop-filter: blur(14px); + border-bottom: 1px solid rgba(24,40,64,0.5); + transition: border-color 0.3s; +} + +@media (min-width: 768px) { + nav { padding: 1.1rem 2rem; } +} + +.nav-logo { + display: flex; + align-items: center; + gap: 0.55rem; + text-decoration: none; +} +.nav-logo-img { + height: 28px; + width: auto; + display: block; + flex-shrink: 0; +} +.nav-logo-text { + font-family: var(--font-head); + font-size: 1.05rem; + font-weight: 800; + color: var(--accent); + letter-spacing: -0.02em; +} +.nav-logo-tld { color: var(--muted); font-size: 0.85rem; } +@media (max-width: 767px) { + .nav-logo-text { display: none; } +} + +.nav-right { + display: flex; + align-items: center; + gap: 1rem; +} + +@media (min-width: 768px) { + .nav-right { gap: 2rem; } +} + +/* Nav links: hidden on mobile, shown on desktop */ +.nav-links { + display: none; + list-style: none; +} + +@media (min-width: 768px) { + .nav-links { + display: flex; + gap: 1.75rem; + } +} + +.nav-links a { + color: var(--text-dim); + text-decoration: none; + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; + transition: color 0.2s; + position: relative; +} +.nav-links a::after { + content: ''; + position: absolute; + bottom: -2px; + inset-inline: 0; + height: 1px; + background: var(--accent); + transform: scaleX(0); + transition: transform 0.2s; +} +.nav-links a:hover { color: var(--accent); } +.nav-links a:hover::after { transform: scaleX(1); } + +/* Lang toggle */ +.lang-toggle { + display: flex; + background: var(--surface); + border: 1px solid var(--border); + overflow: hidden; +} +.lang-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.25rem; + cursor: pointer; + background: transparent; + border: 2px solid transparent; + border-radius: 4px; + transition: border-color 0.2s, opacity 0.2s; + line-height: 1; +} +.lang-btn .fi { + width: 1.4em; + height: 1.4em; + border-radius: 2px; + display: block; +} +.lang-btn.active { + border-color: var(--accent); +} +.lang-btn:not(.active) { opacity: 0.65; } +.lang-btn:not(.active):hover { opacity: 1; } +span.lang-btn { cursor: default; } +a.lang-btn { text-decoration: none; } + +/* Theme toggle */ +.theme-toggle { + background: none; + border: none; + cursor: pointer; + color: var(--text-dim); + font-size: 1rem; + padding: 0.25rem; + display: flex; + align-items: center; + gap: 0.5rem; + transition: color 0.2s; + line-height: 1; +} +.theme-toggle:hover { color: var(--accent); } + +/* Desktop toggle: hide on mobile */ +.theme-toggle--desktop { + display: none; +} +@media (min-width: 768px) { + .theme-toggle--desktop { display: flex; } +} + +/* Mobile toggle item: hide on desktop */ +.nav-theme-item { display: none; } +.nav-theme-item .theme-toggle { + font-size: 0.8rem; + letter-spacing: 0.08em; + text-transform: uppercase; + padding: 0; +} + +/* Hamburger: shown on mobile, hidden on desktop */ +.hamburger { + display: flex; + flex-direction: column; + gap: 5px; + cursor: pointer; + background: none; + border: none; + padding: 4px; +} +.hamburger span { + display: block; + width: 22px; + height: 2px; + background: var(--text-dim); + transition: all 0.3s; +} + +@media (min-width: 768px) { + .hamburger { display: none; } +} + +/* ── Hero ───────────────────────────────────────── */ +#hero { + min-height: 100svh; + display: flex; + align-items: center; + padding-top: 60px; + position: relative; + overflow: hidden; + background: var(--bg); +} + +@media (min-width: 768px) { + #hero { padding-top: 70px; } +} + +/* Matrix canvas */ +#matrix-canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + opacity: 0.13; + pointer-events: none; +} + +/* Scanlines overlay */ +#hero::before { + content: ''; + position: absolute; + inset: 0; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0,0,0,0.07) 2px, + rgba(0,0,0,0.07) 3px + ); + pointer-events: none; + z-index: 1; +} + +/* Ambient radial glow */ +#hero::after { + content: ''; + position: absolute; + top: 35%; + left: 30%; + transform: translate(-50%, -50%); + width: 900px; + height: 600px; + background: radial-gradient(ellipse, rgba(168,85,247,0.07) 0%, transparent 65%); + pointer-events: none; + z-index: 0; +} + +/* Hero inner: flex column — name full-width, then bottom row */ +.hero-inner { + padding: 3rem 0; + display: flex; + flex-direction: column; + gap: 1.5rem; + position: relative; + z-index: 2; +} + +@media (min-width: 768px) { + .hero-inner { padding: 4rem 0; } +} + +/* Bottom row: content + terminal side-by-side on desktop */ +.hero-bottom { + display: flex; + align-items: center; + gap: 3rem; +} + +.hero-content { flex: 1; min-width: 0; } + +/* ── Prompt ── */ +.hero-prompt { + font-size: 0.8rem; + color: var(--accent2); + letter-spacing: 0.1em; + margin-bottom: 1.25rem; + opacity: 0; + animation: fadeUp 0.6s ease forwards 0.2s; +} +.hero-prompt::before { content: '$ '; color: var(--muted); } + +/* ── Name with glitch ── */ +.hero-name { + font-family: var(--font-head); + font-size: clamp(3rem, 12vw, 7rem); + font-weight: 800; + line-height: 0.95; + letter-spacing: -0.04em; + color: var(--text); + margin-bottom: 0; + opacity: 0; + animation: fadeUp 0.6s ease forwards 0.4s; + display: inline-block; + position: relative; + text-shadow: 0 0 80px rgba(168,85,247,0.18), 0 0 120px rgba(168,85,247,0.08); +} + +/* Chromatic aberration layers */ +.hero-name::before, +.hero-name::after { + content: attr(data-text); + position: absolute; + top: 0; left: 0; + width: 100%; + height: 100%; + opacity: 0; + pointer-events: none; + overflow: hidden; + font-family: inherit; + font-size: inherit; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; +} +.hero-name::before { color: #ff2b6d; } +.hero-name::after { color: #00e5ff; } + +.hero-name.is-glitching::before { + opacity: 0.8; + animation: glitch-red 0.45s steps(3) forwards; +} +.hero-name.is-glitching::after { + opacity: 0.8; + animation: glitch-cyn 0.45s steps(3) forwards; +} + +/* ── Role ── */ +.hero-role { + font-size: clamp(0.85rem, 3vw, 1rem); + color: var(--accent); + margin-bottom: 1.5rem; + opacity: 0; + animation: fadeUp 0.6s ease forwards 0.6s; + min-height: 1.75em; +} + +.cursor { + display: inline-block; + width: 2px; + height: 1em; + background: var(--accent); + margin-left: 1px; + vertical-align: text-bottom; + animation: blink 1s step-end infinite; +} + +/* ── Tagline ── */ +.hero-tagline { + font-size: 0.93rem; + color: var(--text-dim); + max-width: 500px; + margin-bottom: 2.5rem; + opacity: 0; + animation: fadeUp 0.6s ease forwards 0.8s; + line-height: 1.95; +} + +/* ── CTA buttons ── */ +.hero-cta { + display: flex; + flex-direction: column; + gap: 1rem; + opacity: 0; + animation: fadeUp 0.6s ease forwards 1s; +} + +@media (min-width: 480px) { + .hero-cta { flex-direction: row; flex-wrap: wrap; } +} + +/* ── Terminal panel ── */ +.hero-term { + display: none; +} + +@media (min-width: 900px) { + .hero-term { + display: flex; + flex-direction: column; + width: 320px; + flex-shrink: 0; + background: rgba(6, 11, 16, 0.85); + border: 1px solid rgba(168,85,247,0.3); + border-radius: 10px; + overflow: hidden; + box-shadow: 0 0 40px rgba(168,85,247,0.1), inset 0 0 30px rgba(0,0,0,0.4); + opacity: 0; + animation: fadeUp 0.6s ease forwards 0.5s; + backdrop-filter: blur(8px); + } +} + +.hero-term-bar { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.6rem 0.9rem; + background: rgba(255,255,255,0.04); + border-bottom: 1px solid rgba(168,85,247,0.15); +} + +.hero-term-dot { + width: 11px; + height: 11px; + border-radius: 50%; + background: var(--c, #888); + flex-shrink: 0; +} + +.hero-term-title { + margin-left: 0.4rem; + font-size: 0.65rem; + color: var(--muted); + letter-spacing: 0.06em; + font-family: var(--font-mono); +} + +.hero-term-body { + padding: 1rem 1.1rem; + font-family: var(--font-mono); + font-size: 0.78rem; + line-height: 1.7; + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +/* Terminal line fade-in sequence */ +.tl { + opacity: 0; + animation: fadeIn 0.25s ease forwards; +} +.hero-term-body .tl { white-space: nowrap; } +.tl-d1 { animation-delay: 0.9s; } +.tl-d2 { animation-delay: 1.5s; } +.tl-d3 { animation-delay: 1.9s; } +.tl-d4 { animation-delay: 2.1s; } +.tl-d5 { animation-delay: 2.3s; } +.tl-d6 { animation-delay: 2.5s; } +.tl-d7 { animation-delay: 2.7s; } +.tl-d8 { animation-delay: 2.9s; } +.tl-d9 { animation-delay: 3.1s; } + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.tc-dim { color: var(--muted); } +.tc-ok { color: var(--accent2); } +.tc-val { color: var(--text); } +.tc-key { color: var(--muted); } +.tc-blink { animation: blink 1s step-end infinite; color: var(--accent2); } + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.65rem 1.4rem; + font-family: var(--font-mono); + font-size: 0.8rem; + letter-spacing: 0.1em; + text-transform: uppercase; + text-decoration: none; + cursor: pointer; + border: none; + transition: all 0.22s; +} + +@media (min-width: 480px) { + .btn { justify-content: flex-start; } +} + +.btn-primary { + background: var(--accent); + color: var(--bg); + font-weight: 700; +} +.btn-primary:hover { + background: #fff; + box-shadow: 0 0 24px rgba(168,85,247,0.45); +} +.btn-outline { + background: transparent; + color: var(--text); + border: 1px solid var(--border); +} +.btn-outline:hover { + border-color: var(--accent); + color: var(--accent); + box-shadow: inset 0 0 16px rgba(168,85,247,0.06); +} +.btn-cv { + background: transparent; + color: var(--muted); + border: 1px dashed var(--border); + gap: 0.45rem; +} +.btn-cv:hover { + border-color: var(--accent); + border-style: solid; + color: var(--accent); +} + +.scroll-indicator { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + color: var(--muted); + font-size: 0.7rem; + letter-spacing: 0.12em; + text-transform: uppercase; + opacity: 0; + animation: fadeUp 0.6s ease forwards 1.4s; +} +.scroll-line { + width: 1px; + height: 44px; + background: linear-gradient(to bottom, var(--accent), transparent); + animation: scanDown 2s ease-in-out infinite; +} + +/* ── Sections ───────────────────────────────────── */ +section { + padding: 4rem 0; + position: relative; + z-index: 1; +} + +@media (min-width: 768px) { + section { padding: 6rem 0; } +} + +.section-eyebrow { + font-size: 0.75rem; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 0.6rem; + display: flex; + align-items: center; + gap: 0.7rem; +} +.section-eyebrow::before { content: '//'; color: var(--muted); } + +.section-title { + font-family: var(--font-head); + font-size: clamp(1.7rem, 6vw, 3rem); + font-weight: 800; + color: var(--text); + letter-spacing: -0.035em; + margin-bottom: 2rem; + line-height: 1.05; +} + +@media (min-width: 768px) { + .section-title { margin-bottom: 3rem; } +} + +/* ── About ──────────────────────────────────────── */ + +/* Mobile: single column */ +.about-grid { + display: grid; + grid-template-columns: 1fr; + gap: 2.5rem; + align-items: start; +} + +/* Desktop: two columns */ +@media (min-width: 768px) { + .about-grid { + grid-template-columns: 1.1fr 0.9fr; + gap: 4rem; + } +} + +.about-bio p { + color: var(--text-dim); + font-size: 0.95rem; + line-height: 1.95; + margin-bottom: 1rem; + hyphens: auto; +} +.about-bio p:last-child { margin-bottom: 0; } + +/* About photo (floated inside bio column) */ +.about-photo { + float: left; + width: 120px; + height: 120px; + object-fit: cover; + object-position: center top; + border: 1px solid var(--border); + margin: 0.2rem 1.25rem 0.75rem 0; + filter: grayscale(15%); + transition: filter 0.4s; + flex-shrink: 0; +} + +.about-photo:hover { + filter: grayscale(0%); +} + +.about-bio { overflow: hidden; } + +/* Terminal window */ +.terminal { + background: var(--bg2); + border: 1px solid var(--border); +} +.terminal-bar { + padding: 0.55rem 1rem; + background: var(--surface); + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 0.45rem; +} +.dot { width: 10px; height: 10px; border-radius: 50%; } +.dot-r { background: #ff5f57; } +.dot-y { background: #ffbd2e; } +.dot-g { background: #28c840; } +.t-file { font-size: 0.68rem; color: var(--muted); margin-left: 0.4rem; } + +.terminal-body { + padding: 1.4rem 1.5rem; + font-size: 0.84rem; + line-height: 1.9; + overflow-x: auto; +} +.tl { display: flex; gap: 0.6rem; } +.t-ps { color: var(--accent2); user-select: none; flex-shrink: 0; } +.t-cm { color: var(--accent); } +.t-out { color: var(--text-dim); padding-left: 1rem; } +.t-k { color: var(--muted); } +.t-v { color: var(--text); } +.t-s { color: var(--accent2); } + +/* ── Services ───────────────────────────────────── */ +.services-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 600px) { + .services-grid { grid-template-columns: repeat(2, 1fr); } +} + +.service-card { + border: 1px solid var(--border); + background: var(--bg2); + padding: 1.75rem; + position: relative; + overflow: hidden; + transition: border-color 0.3s, background 0.2s; +} +.service-card::before { + content: ''; + position: absolute; + top: 0; inset-inline: 0; + height: 2px; + background: linear-gradient(90deg, var(--accent), var(--accent2)); +} +.service-card:hover { + border-color: rgba(168,85,247,0.28); + background: var(--surface); +} + +.service-icon { + font-size: 1.6rem; + margin-bottom: 1rem; + color: var(--accent); + display: block; +} +.service-title { + font-family: var(--font-head); + font-size: 1.1rem; + font-weight: 800; + color: var(--text); + letter-spacing: -0.02em; + margin-bottom: 0.2rem; +} +.service-subtitle { + font-size: 0.76rem; + color: var(--accent); + letter-spacing: 0.06em; + margin-bottom: 0.9rem; +} +.service-desc { + font-size: 0.88rem; + color: var(--text-dim); + line-height: 1.85; +} + +/* ── Education ──────────────────────────────────── */ +#education { background: var(--bg2); } + +.edu-list { + position: relative; + display: flex; + flex-direction: column; + padding-left: 1rem; +} +.edu-list::before { + content: ''; + position: absolute; + left: 0.44rem; + top: 0.75rem; + bottom: 0; + width: 1px; + background: linear-gradient(to bottom, var(--accent), transparent); +} + +.edu-item { + display: grid; + grid-template-columns: 1.5rem 1fr; + gap: 1.5rem; + padding-bottom: 2.5rem; +} +.edu-item:last-child { padding-bottom: 0; } + +.edu-dot { + width: 0.9rem; + height: 0.9rem; + border-radius: 50%; + border: 2px solid var(--accent); + background: var(--bg2); + flex-shrink: 0; + margin-top: 0.3rem; + position: relative; + z-index: 1; + transition: background 0.2s; +} +.edu-dot.current { + background: var(--accent); + box-shadow: 0 0 10px rgba(168,85,247,0.5); +} + +.edu-period { + font-size: 0.72rem; + color: var(--accent); + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 0.35rem; +} +.edu-degree { + font-family: var(--font-head); + font-size: 1.05rem; + font-weight: 800; + color: var(--text); + letter-spacing: -0.02em; + margin-bottom: 0.2rem; +} +.edu-institution { + font-size: 0.78rem; + color: var(--muted); + letter-spacing: 0.03em; + margin-bottom: 0.6rem; +} +.edu-desc { + font-size: 0.88rem; + color: var(--text-dim); + line-height: 1.85; +} + +/* ── Skills ─────────────────────────────────────── */ +#skills { background: var(--bg2); } +#skills::before { + content: ''; + position: absolute; + inset: 0; + background-image: radial-gradient(circle, rgba(168,85,247,0.04) 1px, transparent 1px); + background-size: 28px 28px; +} + +/* Mobile: single column; wider: auto-fill grid */ +.skills-grid { + display: grid; + grid-template-columns: 1fr; + background: var(--border); + gap: 1px; + border: 1px solid var(--border); +} + +@media (min-width: 480px) { + .skills-grid { grid-template-columns: repeat(2, 1fr); } +} + +@media (min-width: 900px) { + .skills-grid { grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); } +} + +.skill-card { + background: var(--bg2); + padding: 1.5rem; + transition: background 0.2s; +} +.skill-card:hover { background: var(--surface); } + +.skill-head { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.6rem; +} +.skill-icon { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + background: var(--accent-glow); + border: 1px solid rgba(168,85,247,0.18); + font-size: 1rem; + flex-shrink: 0; + color: var(--accent); +} +.skill-name { + font-size: 0.85rem; + font-weight: 700; + color: var(--text); + letter-spacing: 0.01em; +} +.skill-track { + height: 2px; + background: var(--border); + margin-top: 0.75rem; + overflow: hidden; +} +.skill-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--accent2)); + transform-origin: left; + transform: scaleX(0); + transition: transform 1.1s cubic-bezier(0.16,1,0.3,1); +} +.skill-fill.go { transform: scaleX(var(--w, 1)); } + +.skill-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + margin-top: 0.75rem; +} +.tag { + font-size: 0.7rem; + padding: 0.18rem 0.45rem; + background: rgba(168,85,247,0.05); + border: 1px solid rgba(168,85,247,0.13); + color: var(--text-dim); + letter-spacing: 0.04em; +} + +/* ── Certifications ─────────────────────────────── */ + +/* Mobile: single column; wider: auto-fill */ +.cert-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 640px) { + .cert-grid { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); } +} + +.cert-card { + border: 1px solid var(--border); + background: var(--bg2); + padding: 1.5rem; + position: relative; + overflow: hidden; + transition: border-color 0.3s, box-shadow 0.3s; +} + +.cert-date { + position: absolute; + top: 1.5rem; + right: 1.5rem; + font-size: 0.72rem; + color: var(--muted); + letter-spacing: 0.06em; +} + +.cert-wip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + padding: 0.2rem 0.55rem; + border-radius: 999px; + background: color-mix(in srgb, var(--accent2, #00ff88) 12%, transparent); + color: var(--accent2, #00ff88); + border: 1px solid color-mix(in srgb, var(--accent2, #00ff88) 35%, transparent); + margin-bottom: 0.75rem; +} + +.cert-wip::before { + content: ""; + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--accent2, #00ff88); + animation: pulse-dot 1.4s ease-in-out infinite; +} + +@keyframes pulse-dot { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.7); } +} + +@media (min-width: 640px) { + .cert-card { padding: 2rem; } +} + +.cert-card::before { + content: ''; + position: absolute; + top: 0; inset-inline: 0; + height: 2px; + background: linear-gradient(90deg, var(--accent), var(--accent2)); +} +.cert-card:hover { + border-color: rgba(168,85,247,0.28); + box-shadow: 0 0 40px rgba(168,85,247,0.04); +} +.cert-card.placeholder { + opacity: 0.38; + border-style: dashed; +} +.cert-card.placeholder::before { display: none; } + +.cert-badge { + width: 72px; + height: 72px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.2rem; + position: relative; +} +.cert-badge--img { + background: transparent; +} +.cert-badge--img img { + width: 100%; + height: 100%; + object-fit: contain; +} +.cert-badge--placeholder { + background: var(--accent-glow); + border: 2px dashed var(--muted); + font-size: 1.6rem; + color: var(--muted); + width: 52px; + height: 52px; +} + +.cert-name { + font-family: var(--font-head); + font-size: 1.35rem; + font-weight: 800; + color: var(--text); + letter-spacing: -0.02em; + margin-bottom: 0.2rem; +} +.cert-issuer { + font-size: 0.78rem; + color: var(--accent); + letter-spacing: 0.06em; + margin-bottom: 0.9rem; +} +.cert-desc { + font-size: 0.88rem; + color: var(--text-dim); + line-height: 1.85; +} +.cert-meta { + display: flex; + flex-wrap: wrap; + gap: 1rem 1.5rem; + margin-top: 1.2rem; + padding-top: 1rem; + border-top: 1px solid var(--border); +} +.cert-meta-item { font-size: 0.76rem; } +.cert-meta-label { color: var(--muted); letter-spacing: 0.06em; display: block; margin-bottom: 0.1rem; } +.cert-meta-val { color: var(--text-dim); font-weight: 700; } + +/* ── Carousel ───────────────────────────────────── */ +.carousel-wrap { position: relative; } + +/* Use double-class specificity to beat section-level grid rules */ +.cert-grid.is-carousel, +.projects-grid.is-carousel { + display: flex; + overflow-x: auto; + scroll-snap-type: x mandatory; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + gap: 1.5rem; + padding-bottom: 0.25rem; +} +.cert-grid.is-carousel::-webkit-scrollbar, +.projects-grid.is-carousel::-webkit-scrollbar { display: none; } + +/* Card sizing — narrow: ~1 card */ +.is-carousel .cert-card, +.is-carousel .project-card { + flex: 0 0 min(300px, 80vw); + scroll-snap-align: start; +} + +/* Tablet: ~2 cards */ +@media (min-width: 640px) { + .is-carousel .cert-card, + .is-carousel .project-card { + flex: 0 0 calc(50% - 0.75rem); + } +} + +/* Wide: revert to grid if not is-wide-carousel */ +@media (min-width: 900px) { + .cert-grid.is-carousel:not(.is-wide-carousel), + .projects-grid.is-carousel:not(.is-wide-carousel) { + display: grid; + overflow: visible; + scroll-snap-type: none; + } + .cert-grid.is-carousel:not(.is-wide-carousel) .cert-card, + .projects-grid.is-carousel:not(.is-wide-carousel) .project-card { + flex: none; + scroll-snap-align: none; + } + /* Wide carousel: 3 cards per view */ + .is-wide-carousel .cert-card, + .is-wide-carousel .project-card { + flex: 0 0 calc(33.333% - 1rem); + scroll-snap-align: start; + } +} + +/* Controls: visible on narrow always, on wide only if is-wide-carousel */ +.carousel-controls { + display: flex; + align-items: center; + justify-content: center; + gap: 1rem; + margin-top: 1.5rem; +} + +@media (min-width: 900px) { + .carousel-wrap:not(:has(.is-wide-carousel)) .carousel-controls { + display: none; + } +} + +.carousel-btn { + width: 36px; + height: 36px; + border-radius: 50%; + border: 1px solid var(--border); + background: var(--bg2); + color: var(--text-dim); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + flex-shrink: 0; + transition: border-color 0.2s, color 0.2s; +} +.carousel-btn:hover { border-color: var(--accent); color: var(--accent); } + +.carousel-dots { + display: flex; + gap: 0.45rem; + align-items: center; +} + +.carousel-dot { + width: 6px; + height: 6px; + border-radius: 50%; + border: none; + padding: 0; + background: var(--border); + cursor: pointer; + transition: background 0.2s, transform 0.2s; +} +.carousel-dot.active { background: var(--accent); transform: scale(1.4); } + +/* ── Projects ───────────────────────────────────── */ +.projects-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1.5rem; +} + +@media (min-width: 640px) { + .projects-grid { grid-template-columns: repeat(2, 1fr); } +} + +@media (min-width: 900px) { + .projects-grid { grid-template-columns: repeat(3, 1fr); } +} + +.project-card { + background: var(--bg2); + border: 1px solid var(--border); + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + transition: border-color 0.25s, transform 0.25s; +} + +.project-card:hover { + border-color: var(--accent); + transform: translateY(-3px); +} + +.project-img-wrap { + width: 100%; + aspect-ratio: 16 / 9; + overflow: hidden; + background: var(--bg3, var(--bg2)); +} + +.project-img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + transition: transform 0.4s; +} + +.project-card:hover .project-img { transform: scale(1.04); } + +.project-body { + padding: 1.25rem; + display: flex; + flex-direction: column; + flex: 1; + gap: 0.6rem; +} + +.project-title { + font-size: 1rem; + font-weight: 700; + color: var(--text); + margin: 0; +} + +.project-subtitle { + font-size: 0.8rem; + color: var(--accent); + letter-spacing: 0.06em; + text-transform: uppercase; + margin: 0; +} + +.project-tags { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.project-tag { + font-size: 0.75rem; + padding: 0.2rem 0.55rem; + border-radius: 999px; + background: color-mix(in srgb, var(--accent) 12%, transparent); + color: var(--accent); + border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); + letter-spacing: 0.04em; +} + +.project-desc { + font-size: 0.88rem; + color: var(--text-dim); + line-height: 1.7; + margin: 0; + flex: 1; +} + +.project-footer { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 0.6rem; + padding-top: 0.75rem; + border-top: 1px solid var(--border); + gap: 0.5rem; +} + +.project-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + font-size: 0.84rem; + font-weight: 600; + color: var(--accent); + text-decoration: none; + transition: opacity 0.2s; +} + +.project-link:hover { opacity: 0.75; } + +.project-share { + display: flex; + gap: 0.5rem; +} + +.share-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + background: color-mix(in srgb, var(--text-dim) 8%, transparent); + color: var(--text-dim); + text-decoration: none; + font-size: 0.78rem; + transition: background 0.2s, color 0.2s; +} + +.share-btn:hover { + background: color-mix(in srgb, var(--accent) 15%, transparent); + color: var(--accent); +} + +.project-card--collab { + border-style: dashed; + border-color: color-mix(in srgb, var(--accent) 35%, transparent); + background: color-mix(in srgb, var(--accent) 4%, transparent); + justify-content: center; + min-height: 220px; +} + +.project-card--collab:hover { + border-style: dashed; + border-color: var(--accent); + transform: translateY(-3px); +} + +.project-collab-icon { + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + color: color-mix(in srgb, var(--accent) 45%, transparent); + padding: 1.5rem 1.25rem 0; + transition: color 0.25s; +} + +.project-card--collab:hover .project-collab-icon { + color: var(--accent); +} + +/* ── Contact ────────────────────────────────────── */ +#contact { background: var(--bg2); } + +.contact-sub { + font-size: 0.93rem; + color: var(--text-dim); + max-width: 520px; + margin-bottom: 2rem; + line-height: 1.95; +} + +@media (min-width: 768px) { + .contact-sub { margin-bottom: 2.5rem; } +} + +.contact-list { + border: 1px solid var(--border); + max-width: 580px; +} +.contact-row { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.9rem 1.2rem; + text-decoration: none; + cursor: pointer; + color: var(--text); + border-bottom: 1px solid var(--border); + transition: background 0.2s, color 0.2s; +} + +@media (min-width: 480px) { + .contact-row { padding: 1rem 1.4rem; } +} + +.contact-row:last-child { border-bottom: none; } +.contact-row:hover { background: var(--accent-glow); } + +.contact-ico { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + background: var(--surface); + border: 1px solid var(--border); + flex-shrink: 0; + font-size: 1rem; + color: var(--text-dim); + transition: border-color 0.2s, color 0.2s; +} +.contact-row:hover .contact-ico { color: var(--accent); } +.contact-row:hover .contact-ico { border-color: rgba(168,85,247,0.35); } +.contact-lbl { font-size: 0.72rem; color: var(--muted); letter-spacing: 0.06em; display: block; } +.contact-val { font-size: 0.88rem; color: var(--text); } +.contact-row:hover .contact-val { color: var(--accent); } + +/* Contact form max width when standalone */ +.contact-form { max-width: 640px; } + +/* ── Contact form ───────────────────────────────── */ +.contact-form { + flex: 1; + display: flex; + flex-direction: column; + gap: 1.1rem; +} + +/* Honeypot — visually hidden, not display:none so bots still see it */ +.cf-honey { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +.cf-row { + display: grid; + grid-template-columns: 1fr; + gap: 1.1rem; +} + +@media (min-width: 480px) { + .cf-row { grid-template-columns: 1fr 1fr; } +} + +.cf-field { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.cf-field label { + font-size: 0.76rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); +} + +.cf-req { color: var(--accent); } + +.cf-field input, +.cf-field textarea { + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + font-family: var(--font-mono); + font-size: 0.88rem; + padding: 0.65rem 0.9rem; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + resize: vertical; + width: 100%; +} + +.cf-field input:focus, +.cf-field textarea:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(168,85,247,0.1); +} + +.cf-invalid { + border-color: #ff5f7e !important; + box-shadow: 0 0 0 3px rgba(255,95,126,0.1) !important; +} + +.cf-field input::placeholder, +.cf-field textarea::placeholder { + color: var(--muted); +} + +.cf-footer { + display: flex; + align-items: center; + gap: 1.2rem; + flex-wrap: wrap; +} + +.cf-submit { min-width: 160px; } +.cf-submit.loading ~ .cf-clear { visibility: hidden; } + +.cf-submit.loading span { display: none; } +.cf-submit.loading::after { + content: ''; + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid currentColor; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.7s linear infinite; +} + +@keyframes spin { to { transform: rotate(360deg); } } + +.cf-status { + font-size: 0.84rem; + line-height: 1.4; +} + +.cf-status.ok { color: var(--accent2); } +.cf-status.err { color: #ff5f7e; } + +/* ── Footer ─────────────────────────────────────── */ +footer { + padding: 1.5rem 1.2rem; + text-align: center; + font-size: 0.75rem; + color: var(--muted); + letter-spacing: 0.07em; + border-top: 1px solid var(--border); + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +@media (min-width: 768px) { + footer { padding: 2rem; } +} + +.footer-top { color: var(--text-dim); } +.footer-copy { color: var(--muted); font-size: 0.7rem; } + +.footer-social { + display: flex; + align-items: center; + justify-content: center; + gap: 0.6rem; +} + +.footer-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 50%; + border: 1px solid var(--border); + color: var(--muted); + font-size: 0.8rem; + text-decoration: none; + cursor: pointer; + transition: border-color 0.2s, color 0.2s; +} + +.footer-icon:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* ── Scroll Reveal ──────────────────────────────── */ +.reveal { + opacity: 0; + transform: translateY(28px); + transition: opacity 0.75s cubic-bezier(0.16,1,0.3,1), + transform 0.75s cubic-bezier(0.16,1,0.3,1); +} +.reveal.visible { opacity: 1; transform: translateY(0); } + +/* ── Keyframes ──────────────────────────────────── */ +@keyframes fadeUp { + from { opacity: 0; transform: translateY(22px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } +} + +@keyframes scanDown { + 0%, 100% { opacity: 0.25; transform: scaleY(0.5); transform-origin: top; } + 50% { opacity: 1; transform: scaleY(1); } +} + +@keyframes glitch { + 0% { transform: translate(0); clip-path: none; } + 20% { transform: translate(-3px, 2px); clip-path: inset(25% 0 35% 0); } + 40% { transform: translate(3px, -2px); clip-path: inset(55% 0 15% 0); } + 60% { transform: translate(-2px, 1px); clip-path: inset(10% 0 65% 0); } + 80% { transform: translate(2px, -1px); clip-path: inset(75% 0 8% 0); } + 100% { transform: translate(0); clip-path: none; } +} + +@keyframes glitch-red { + 0% { transform: translate(-5px, 1px); clip-path: inset(8% 0 78% 0); } + 33% { transform: translate(4px, -2px); clip-path: inset(42% 0 38% 0); } + 66% { transform: translate(-3px, 2px); clip-path: inset(68% 0 12% 0); } + 100% { transform: translate(0); clip-path: inset(0 0 100% 0); opacity: 0; } +} + +@keyframes glitch-cyn { + 0% { transform: translate(5px, -1px); clip-path: inset(22% 0 62% 0); } + 33% { transform: translate(-4px, 2px); clip-path: inset(55% 0 28% 0); } + 66% { transform: translate(3px, -2px); clip-path: inset(15% 0 70% 0); } + 100% { transform: translate(0); clip-path: inset(0 0 100% 0); opacity: 0; } +} + +/* ── Accessibility: Focus Styles ────────────────── */ +.nav-links a:focus-visible, +.btn:focus-visible, +.lang-btn:focus-visible, +.footer-icon:focus-visible, +.carousel-btn:focus-visible, +.carousel-dot:focus-visible, +.hamburger:focus-visible, +.project-link:focus-visible, +.share-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} + +/* ── Accessibility: Reduced Motion ─────────────── */ +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + + /* Reveal elements should be visible immediately */ + .reveal { + opacity: 1; + transform: none; + transition: none; + } + + /* Hero animated elements should start visible */ + .hero-prompt, + .hero-name, + .hero-role, + .hero-tagline, + .hero-cta, + .hero-term, + .scroll-indicator { + opacity: 1; + animation: none; + } + + /* Terminal lines should be visible immediately */ + .tl { opacity: 1; animation: none; } + + /* Stop blinking cursors */ + .cursor, + .tc-blink { animation: none; opacity: 1; } + + /* Hide matrix canvas */ + #matrix-canvas { display: none; } +} diff --git a/themes/danixme/assets/js/main.js b/themes/danixme/assets/js/main.js new file mode 100644 index 0000000..7d28689 --- /dev/null +++ b/themes/danixme/assets/js/main.js @@ -0,0 +1,374 @@ +/* ── Matrix Rain ─────────────────────────────────── */ +(function () { + const canvas = document.getElementById('matrix-canvas'); + if (!canvas) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + + const ctx = canvas.getContext('2d'); + const CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF<>/\\|{}[]$#@!'; + const FS = 14; + let cols, drops, raf; + + function init() { + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + cols = Math.floor(canvas.width / FS) + 1; + drops = Array.from({ length: cols }, () => Math.random() * -(canvas.height / FS)); + } + + function tick() { + const light = document.documentElement.classList.contains('theme-light'); + ctx.fillStyle = light ? 'rgba(240,244,248,0.07)' : 'rgba(6,11,16,0.055)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.font = `${FS}px "JetBrains Mono", monospace`; + + for (let i = 0; i < cols; i++) { + const char = CHARS[Math.floor(Math.random() * CHARS.length)]; + // Occasional bright green "head", otherwise purple + ctx.fillStyle = Math.random() > 0.96 + ? (light ? '#008f5a' : '#00ff88') + : (light ? '#7c3aed' : '#a855f7'); + ctx.fillText(char, i * FS, drops[i] * FS); + + if (drops[i] * FS > canvas.height && Math.random() > 0.975) { + drops[i] = Math.random() * -20; + } + drops[i] += 0.5; + } + raf = requestAnimationFrame(tick); + } + + init(); + window.addEventListener('resize', () => { cancelAnimationFrame(raf); init(); tick(); }, { passive: true }); + document.addEventListener('visibilitychange', () => { + if (document.hidden) cancelAnimationFrame(raf); else tick(); + }); + tick(); +})(); + +/* ── Hero Glitch ─────────────────────────────────── */ +(function () { + const name = document.querySelector('.hero-name'); + if (!name) return; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + + function glitch() { + name.classList.add('is-glitching'); + setTimeout(() => name.classList.remove('is-glitching'), 500); + setTimeout(glitch, 4000 + Math.random() * 7000); + } + setTimeout(glitch, 3500); +})(); + +/* ── CV Download ─────────────────────────────────── */ +document.querySelectorAll('[data-cv]').forEach(el => { + const trigger = (e) => { + e.preventDefault(); + const a = document.createElement('a'); + a.href = atob(el.dataset.cv); + a.download = ''; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }; + el.addEventListener('click', trigger); + el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') trigger(e); }); +}); + +/* ── Email Assembly ──────────────────────────────── */ +document.querySelectorAll('[data-eu]').forEach(el => { + const addr = el.dataset.eu + '@' + el.dataset.ed; + el.href = 'mailto:' + addr; + const val = el.querySelector('.contact-val'); + if (val) val.textContent = addr; +}); + +/* ── Typing Animation ────────────────────────────── */ +const typedEl = document.getElementById('typed'); +const phrases = window.SITE_PHRASES || []; + +let pIdx = 0, cIdx = 0, deleting = false, timer; + +function type() { + const word = phrases[pIdx % phrases.length]; + if (!word) return; + + if (!deleting) { + typedEl.textContent = word.slice(0, ++cIdx); + if (cIdx === word.length) { deleting = true; timer = setTimeout(type, 2200); return; } + } else { + typedEl.textContent = word.slice(0, --cIdx); + if (cIdx === 0) { deleting = false; pIdx++; timer = setTimeout(type, 350); return; } + } + timer = setTimeout(type, deleting ? 38 : 68); +} + +function resetTyping() { + clearTimeout(timer); + pIdx = 0; cIdx = 0; deleting = false; + typedEl.textContent = ''; + type(); +} + +resetTyping(); + +/* ── Scroll Reveal ───────────────────────────────── */ +const revealObs = new IntersectionObserver((entries) => { + entries.forEach((e, i) => { + if (e.isIntersecting) { + setTimeout(() => e.target.classList.add('visible'), i * 90); + revealObs.unobserve(e.target); + } + }); +}, { threshold: 0.1 }); + +document.querySelectorAll('.reveal').forEach(el => revealObs.observe(el)); + +/* ── Skill Bar Animation ─────────────────────────── */ +const skillsObs = new IntersectionObserver((entries) => { + entries.forEach(e => { + if (e.isIntersecting) { + e.target.querySelectorAll('.skill-fill').forEach((bar, i) => + setTimeout(() => bar.classList.add('go'), i * 110) + ); + skillsObs.unobserve(e.target); + } + }); +}, { threshold: 0.2 }); + +const sg = document.getElementById('skillsGrid'); +if (sg) skillsObs.observe(sg); + +/* ── Carousel ────────────────────────────────────── */ +document.querySelectorAll('[data-carousel]').forEach(wrap => { + const track = wrap.querySelector('.is-carousel'); + const prevBtn = wrap.querySelector('.carousel-btn--prev'); + const nextBtn = wrap.querySelector('.carousel-btn--next'); + const dotsWrap = wrap.querySelector('.carousel-dots'); + if (!track) return; + + const cards = Array.from(track.children); + + // Scroll position of card i relative to the track's scrollable content + function cardScrollLeft(i) { + const trackRect = track.getBoundingClientRect(); + const cardRect = cards[i].getBoundingClientRect(); + return track.scrollLeft + (cardRect.left - trackRect.left); + } + + // Build dots + const dots = cards.map((_, i) => { + const dot = document.createElement('button'); + dot.className = 'carousel-dot'; + dot.setAttribute('aria-label', `${i + 1}`); + dot.addEventListener('click', () => { + track.scrollTo({ left: cardScrollLeft(i), behavior: 'smooth' }); + }); + dotsWrap.appendChild(dot); + return dot; + }); + + function activeDot() { + const trackLeft = track.getBoundingClientRect().left; + let closest = 0, minDist = Infinity; + cards.forEach((card, i) => { + const dist = Math.abs(card.getBoundingClientRect().left - trackLeft); + if (dist < minDist) { minDist = dist; closest = i; } + }); + dots.forEach((d, i) => d.classList.toggle('active', i === closest)); + } + + function cardWidth() { + const gap = parseFloat(getComputedStyle(track).columnGap) || 24; + return cards[0].offsetWidth + gap; + } + + prevBtn.addEventListener('click', () => track.scrollBy({ left: -cardWidth(), behavior: 'smooth' })); + nextBtn.addEventListener('click', () => track.scrollBy({ left: cardWidth(), behavior: 'smooth' })); + + track.addEventListener('scroll', activeDot, { passive: true }); + activeDot(); +}); + +/* ── Contact Form ────────────────────────────────── */ +(function () { + const form = document.getElementById('contact-form'); + if (!form) return; + + // Stamp load time into hidden field — used by PHP timing check + const timeField = document.getElementById('cf-time'); + if (timeField) timeField.value = Math.floor(Date.now() / 1000); + + const btn = form.querySelector('.cf-submit'); + const status = form.querySelector('.cf-status'); + const i18n = window.FORM_I18N || {}; + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + + // Basic client-side required check + const required = form.querySelectorAll('[required]'); + let valid = true; + required.forEach(el => { + el.classList.toggle('cf-invalid', !el.value.trim()); + if (!el.value.trim()) valid = false; + }); + if (!valid) { + setStatus('err', i18n.error_fields); + return; + } + + btn.classList.add('loading'); + btn.disabled = true; + setStatus('', ''); + + try { + const res = await fetch('/api/contact.php', { + method: 'POST', + body: new FormData(form), + }); + const data = await res.json(); + + if (data.success) { + setStatus('ok', i18n.success); + form.reset(); + // Re-stamp time after reset + if (timeField) timeField.value = Math.floor(Date.now() / 1000); + } else { + setStatus('err', i18n.error); + } + } catch { + setStatus('err', i18n.error); + } finally { + btn.classList.remove('loading'); + btn.disabled = false; + } + }); + + // Clear invalid state on input + form.querySelectorAll('input, textarea').forEach(el => { + el.addEventListener('input', () => el.classList.remove('cf-invalid')); + }); + + // On reset: clear status message, invalid states, and re-stamp time + form.addEventListener('reset', () => { + setStatus('', ''); + form.querySelectorAll('.cf-invalid').forEach(el => el.classList.remove('cf-invalid')); + if (timeField) setTimeout(() => { timeField.value = Math.floor(Date.now() / 1000); }, 0); + }); + + function setStatus(type, msg) { + status.className = 'cf-status' + (type ? ' ' + type : ''); + status.textContent = msg; + } +})(); + +/* ── Scroll Progress Bar ─────────────────────────── */ +(function () { + const bar = document.getElementById('scroll-progress'); + if (!bar) return; + window.addEventListener('scroll', () => { + const scrolled = document.documentElement.scrollTop; + const total = document.documentElement.scrollHeight - document.documentElement.clientHeight; + const pct = total > 0 ? (scrolled / total) * 100 : 0; + bar.style.width = pct + '%'; + bar.setAttribute('aria-valuenow', Math.round(pct)); + }, { passive: true }); +})(); + +/* ── Nav Border on Scroll ────────────────────────── */ +const navEl = document.getElementById('nav'); +const isLight = () => document.documentElement.classList.contains('theme-light'); +window.addEventListener('scroll', () => { + navEl.style.borderBottomColor = scrollY > 60 + ? (isLight() ? 'rgba(168,189,216,0.9)' : 'rgba(24,40,64,0.9)') + : (isLight() ? 'rgba(168,189,216,0.5)' : 'rgba(24,40,64,0.5)'); +}, { passive: true }); + +/* ── Mobile Menu ─────────────────────────────────── */ +const burger = document.querySelector('.hamburger'); +const navLinks = document.querySelector('.nav-links'); +let menuOpen = false; + +function menuBg() { + return isLight() + ? { bg: 'rgba(240,244,248,0.97)', border: '1px solid rgba(168,189,216,0.8)' } + : { bg: 'rgba(6,11,16,0.97)', border: '1px solid rgba(24,40,64,0.9)' }; +} + +function openMenu() { + menuOpen = true; + burger.setAttribute('aria-expanded', true); + const { bg, border } = menuBg(); + Object.assign(navLinks.style, { + display: 'flex', + flexDirection: 'column', + position: 'absolute', + top: '100%', + left: '0', + right: '0', + background: bg, + padding: '1rem 2rem', + borderBottom: border, + gap: '1rem' + }); + navLinks.querySelector('.nav-theme-item').style.display = 'flex'; +} + +function closeMenu() { + menuOpen = false; + burger.setAttribute('aria-expanded', false); + navLinks.style.display = 'none'; + navLinks.querySelector('.nav-theme-item').style.display = ''; +} + +burger.addEventListener('click', (e) => { + e.stopPropagation(); + menuOpen ? closeMenu() : openMenu(); +}); + +document.addEventListener('click', (e) => { + if (menuOpen && !navEl.contains(e.target)) closeMenu(); +}); + +document.addEventListener('touchstart', (e) => { + if (menuOpen && !navEl.contains(e.target)) closeMenu(); +}, { passive: true }); + +/* ── Theme Toggle ────────────────────────────────── */ +(function () { + const html = document.documentElement; + const toggles = document.querySelectorAll('.theme-toggle'); + + function currentTheme() { + return html.classList.contains('theme-light') ? 'light' : 'dark'; + } + + function applyTheme(theme) { + html.classList.remove('theme-light', 'theme-dark'); + html.classList.add('theme-' + theme); + localStorage.setItem('theme', theme); + updateIcons(); + // Refresh open mobile menu background + if (menuOpen) { + const { bg, border } = menuBg(); + navLinks.style.background = bg; + navLinks.style.borderBottom = border; + } + } + + function updateIcons() { + const light = currentTheme() === 'light'; + toggles.forEach(btn => { + btn.querySelector('i').className = light ? 'fa-solid fa-moon' : 'fa-solid fa-sun'; + const lbl = btn.querySelector('.theme-toggle-label'); + if (lbl) lbl.textContent = light ? 'Dark mode' : 'Light mode'; + }); + } + + toggles.forEach(btn => btn.addEventListener('click', () => { + applyTheme(currentTheme() === 'light' ? 'dark' : 'light'); + })); + + updateIcons(); +})(); diff --git a/themes/danixme/i18n/en.toml b/themes/danixme/i18n/en.toml new file mode 100644 index 0000000..8a7ffd8 --- /dev/null +++ b/themes/danixme/i18n/en.toml @@ -0,0 +1,149 @@ +[nav_about] +other = "About" + +[nav_services] +other = "Services" + +[nav_education] +other = "Education" + +[nav_skills] +other = "Skills" + +[nav_certs] +other = "Certs" + +[nav_contact] +other = "Contact" + +[btn_contact] +other = "↗ Get in Touch" + +[btn_cv] +other = "Download CV" + +[btn_more] +other = "→ Learn More" + +[scroll] +other = "scroll" + +[hero_prompt] +other = "whoami" + +[section_about] +other = "About" + +[section_skills] +other = "Skills" + +[section_certs] +other = "Certifications" + +[section_contact] +other = "Contact" + +[about_title] +other = "Background" + +[section_services] +other = "Services" + +[services_title] +other = "What I Do" + +[section_education] +other = "Education" + +[education_title] +other = "Academic Path" + +[skills_title] +other = "Toolkit" + +[certs_title] +other = "Credentials" + +[contact_title] +other = "Let's Connect" + +[terminal_role] +other = "CyberSecurity Specialist" + +[terminal_status] +other = "open to work" + +[cert_in_progress] +other = "Studying" + +[cert_issuer_label] +other = "Issuer" + +[cert_level_label] +other = "Level" + +[cert_type_label] +other = "Type" + +[cert_date_label] +other = "Obtained" + +[nav_projects] +other = "Projects" + +[section_projects] +other = "Projects" + +[projects_title] +other = "My Work" + +[project_visit] +other = "View Project" + +[carousel_prev] +other = "Previous" + +[carousel_next] +other = "Next" + +[project_collab_title] +other = "Next Project" + +[project_collab_desc] +other = "Got an exciting idea? I'm always open to collaborating on something new. Let's build something great together." + +[project_collab_cta] +other = "Get in Touch" + +[form_name] +other = "Name" + +[form_email] +other = "Email" + +[form_subject] +other = "Subject" + +[form_message] +other = "Message" + +[form_send] +other = "Send Message" + +[form_clear] +other = "Clear" + +[form_success] +other = "Message sent! I'll get back to you soon." + +[form_error] +other = "Something went wrong. Please try again." + +[form_error_fields] +other = "Please fill in all required fields." + +[footer1] +other = 'Made with a mix of <i class="fa-solid fa-heart"></i> <i class="fa-solid fa-mug-saucer"></i> and a general lack of <i class="fa-solid fa-bed"></i>, plus some mad <i class="fa-solid fa-terminal"></i> skills' + +[footer2] +other = "<em>This site is what my 15 years old self wanted to build</em>" diff --git a/themes/danixme/i18n/it.toml b/themes/danixme/i18n/it.toml new file mode 100644 index 0000000..943119d --- /dev/null +++ b/themes/danixme/i18n/it.toml @@ -0,0 +1,149 @@ +[nav_about] +other = "Chi Sono" + +[nav_services] +other = "Servizi" + +[nav_education] +other = "Formazione" + +[nav_skills] +other = "Competenze" + +[nav_certs] +other = "Certificazioni" + +[nav_contact] +other = "Contatti" + +[btn_contact] +other = "↗ Contattami" + +[btn_cv] +other = "Scarica CV" + +[btn_more] +other = "→ Scopri di Più" + +[scroll] +other = "scorri" + +[hero_prompt] +other = "chi_sono" + +[section_about] +other = "Chi Sono" + +[section_skills] +other = "Competenze" + +[section_certs] +other = "Certificazioni" + +[section_contact] +other = "Contatti" + +[about_title] +other = "Background" + +[section_services] +other = "Servizi" + +[services_title] +other = "di cosa mi occupo" + +[section_education] +other = "Formazione" + +[education_title] +other = "Percorso Accademico" + +[skills_title] +other = "Strumenti" + +[certs_title] +other = "Credenziali" + +[contact_title] +other = "Connettiti" + +[terminal_role] +other = "Specialista Cybersecurity" + +[terminal_status] +other = "disponibile" + +[cert_in_progress] +other = "In corso" + +[cert_issuer_label] +other = "Ente" + +[cert_level_label] +other = "Livello" + +[cert_type_label] +other = "Tipo" + +[cert_date_label] +other = "Ottenuta" + +[nav_projects] +other = "Progetti" + +[section_projects] +other = "Progetti" + +[projects_title] +other = "I Miei Lavori" + +[project_visit] +other = "Vedi Progetto" + +[carousel_prev] +other = "Precedente" + +[carousel_next] +other = "Successivo" + +[project_collab_title] +other = "Prossimo Progetto" + +[project_collab_desc] +other = "Hai un'idea interessante? Sono sempre aperto a collaborare su qualcosa di nuovo. Costruiamo qualcosa di grande insieme." + +[project_collab_cta] +other = "Contattami" + +[form_name] +other = "Nome" + +[form_email] +other = "Email" + +[form_subject] +other = "Oggetto" + +[form_message] +other = "Messaggio" + +[form_send] +other = "Invia Messaggio" + +[form_clear] +other = "Cancella" + +[form_success] +other = "Messaggio inviato! Ti risponderò presto." + +[form_error] +other = "Qualcosa è andato storto. Riprova." + +[form_error_fields] +other = "Compila tutti i campi obbligatori." + +[footer1] +other = 'Realizzato con un misto di <i class="fa-solid fa-heart"></i> <i class="fa-solid fa-mug-saucer"></i> e una mancanza di <i class="fa-solid fa-bed"></i>, oltre alle mie capacità da <i class="fa-solid fa-terminal"></i>' + +[footer2] +other = "<em>Questo è il sito che sognavo a 15 anni.</em>" diff --git a/themes/danixme/layouts/_default/baseof.html b/themes/danixme/layouts/_default/baseof.html new file mode 100644 index 0000000..f01a9f2 --- /dev/null +++ b/themes/danixme/layouts/_default/baseof.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html lang="{{ .Site.Language.Lang }}"> +{{ partial "head.html" . }} +<body> +<div id="scroll-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"></div> +{{ partial "nav.html" . }} +{{ block "main" . }}{{ end }} +{{ partial "footer.html" . }} +{{ partial "scripts.html" . }} +</body> +</html> diff --git a/themes/danixme/layouts/index.html b/themes/danixme/layouts/index.html new file mode 100644 index 0000000..01e2f90 --- /dev/null +++ b/themes/danixme/layouts/index.html @@ -0,0 +1,10 @@ +{{ define "main" }} +{{ partial "hero.html" . }} +{{ partial "about.html" . }} +{{ partial "services.html" . }} +{{ partial "skills.html" . }} +{{ partial "education.html" . }} +{{ partial "certs.html" . }} +{{ partial "projects.html" . }} +{{ partial "contact.html" . }} +{{ end }} diff --git a/themes/danixme/layouts/partials/about.html b/themes/danixme/layouts/partials/about.html new file mode 100644 index 0000000..68cb6e8 --- /dev/null +++ b/themes/danixme/layouts/partials/about.html @@ -0,0 +1,37 @@ +<section id="about"> + <div class="container"> + <div class="section-eyebrow reveal">{{ i18n "section_about" }}</div> + <h2 class="section-title reveal">{{ i18n "about_title" }}</h2> + + <div class="about-grid"> + <div class="about-bio reveal"> + {{ with .Params.about_photo }} + <img class="about-photo" src="{{ . }}" alt="{{ $.Site.Params.name }}" loading="lazy"> + {{ end }} + <div>{{ .Params.bio | markdownify }}</div> + </div> + + <div class="terminal reveal"> + <div class="terminal-bar"> + <div class="dot dot-r"></div> + <div class="dot dot-y"></div> + <div class="dot dot-g"></div> + <span class="t-file">profile.json</span> + </div> + <div class="terminal-body"> + <div class="tl"><span class="t-ps">~</span><span class="t-cm">cat profile.json</span></div> + <div style="margin-top:.5rem"> + <div class="tl"><span class="t-out">{</span></div> + <div class="tl"><span class="t-out"> <span class="t-k">"name":</span> <span class="t-v">"{{ .Site.Params.name }}",</span></span></div> + <div class="tl"><span class="t-out"> <span class="t-k">"role":</span> <span class="t-v">"{{ i18n "terminal_role" }}",</span></span></div> + <div class="tl"><span class="t-out"> <span class="t-k">"cert":</span> <span class="t-v">"{{ .Site.Params.cert }}",</span></span></div> + <div class="tl"><span class="t-out"> <span class="t-k">"focus":</span> <span class="t-v">"{{ .Site.Params.focus }}",</span></span></div> + <div class="tl"><span class="t-out"> <span class="t-k">"location":</span> <span class="t-v">"{{ .Site.Params.location }}",</span></span></div> + <div class="tl"><span class="t-out"> <span class="t-k">"status":</span> <span class="t-s">"{{ i18n "terminal_status" }}"</span></span></div> + <div class="tl"><span class="t-out">}</span></div> + </div> + </div> + </div> + </div> + </div> +</section> diff --git a/themes/danixme/layouts/partials/certs.html b/themes/danixme/layouts/partials/certs.html new file mode 100644 index 0000000..41e672a --- /dev/null +++ b/themes/danixme/layouts/partials/certs.html @@ -0,0 +1,67 @@ +<section id="certifications"> + <div class="container"> + <div class="section-eyebrow reveal">{{ i18n "section_certs" }}</div> + <h2 class="section-title reveal">{{ i18n "certs_title" }}</h2> + + {{ $lang := .Site.Language.Lang }} + {{ $count := len hugo.Data.certs }} + {{ $wide := gt $count 3 }} + + <div class="carousel-wrap" data-carousel> + <div class="cert-grid is-carousel{{ if $wide }} is-wide-carousel{{ end }}"> + {{ range hugo.Data.certs }} + {{ $ph := .placeholder }} + {{ $ip := index . "in_progress" }} + <div class="cert-card{{ if $ph }} placeholder{{ end }} reveal"> + {{ if $ip }}<span class="cert-wip">{{ i18n "cert_in_progress" }}</span>{{ end }} + {{ with .date }}<span class="cert-date">{{ . }}</span>{{ end }} + {{ $img := index . "image" }} + {{ if and $img (not $ph) }} + <div class="cert-badge cert-badge--img"> + <img src="{{ $img }}" alt="{{ .name }}"> + </div> + {{ else }} + <div class="cert-badge cert-badge--placeholder"><i class="{{ .badge }}"></i></div> + {{ end }} + + <div class="cert-name"{{ if $ph }} style="color:var(--muted)"{{ end }}> + {{ if .name }}{{ .name }}{{ else if eq $lang "it" }}{{ .name_it }}{{ else }}{{ .name_en }}{{ end }} + </div> + + <div class="cert-issuer"{{ if $ph }} style="color:var(--muted)"{{ end }}> + {{ if .issuer }}{{ .issuer }}{{ else if eq $lang "it" }}{{ .issuer_it }}{{ else }}{{ .issuer_en }}{{ end }} + </div> + + <p class="cert-desc">{{ if eq $lang "it" }}{{ .desc_it }}{{ else }}{{ .desc_en }}{{ end }}</p> + + {{ if not $ph }} + <div class="cert-meta"> + <div class="cert-meta-item"> + <span class="cert-meta-label">{{ i18n "cert_issuer_label" }}</span> + <span class="cert-meta-val">{{ .issuer_val }}</span> + </div> + <div class="cert-meta-item"> + <span class="cert-meta-label">{{ i18n "cert_level_label" }}</span> + <span class="cert-meta-val">{{ if eq $lang "it" }}{{ .level_it }}{{ else }}{{ .level_en }}{{ end }}</span> + </div> + <div class="cert-meta-item"> + <span class="cert-meta-label">{{ i18n "cert_type_label" }}</span> + <span class="cert-meta-val">{{ if eq $lang "it" }}{{ .type_it }}{{ else }}{{ .type_en }}{{ end }}</span> + </div> + </div> + {{ end }} + </div> + {{ end }} + </div> + <div class="carousel-controls"> + <button class="carousel-btn carousel-btn--prev" aria-label="{{ i18n "carousel_prev" }}"> + <i class="fa-solid fa-chevron-left"></i> + </button> + <div class="carousel-dots"></div> + <button class="carousel-btn carousel-btn--next" aria-label="{{ i18n "carousel_next" }}"> + <i class="fa-solid fa-chevron-right"></i> + </button> + </div> + </div>{{/* end carousel-wrap */}} + </div> +</section> diff --git a/themes/danixme/layouts/partials/contact.html b/themes/danixme/layouts/partials/contact.html new file mode 100644 index 0000000..9584c74 --- /dev/null +++ b/themes/danixme/layouts/partials/contact.html @@ -0,0 +1,49 @@ +<section id="contact"> + <div class="container"> + <div class="section-eyebrow reveal">{{ i18n "section_contact" }}</div> + <h2 class="section-title reveal">{{ i18n "contact_title" }}</h2> + + <p class="contact-sub reveal">{{ .Params.contact_sub }}</p> + + <form id="contact-form" class="contact-form reveal" novalidate> + <!-- Honeypot: hidden from humans, filled by bots --> + <div class="cf-honey" aria-hidden="true"> + <label for="cf-website">Website</label> + <input type="text" id="cf-website" name="website" tabindex="-1" autocomplete="off"> + </div> + <input type="hidden" name="_t" id="cf-time"> + + <div class="cf-row"> + <div class="cf-field"> + <label for="cf-name">{{ i18n "form_name" }} <span class="cf-req">*</span></label> + <input type="text" id="cf-name" name="name" required autocomplete="name" maxlength="100"> + </div> + <div class="cf-field"> + <label for="cf-email">{{ i18n "form_email" }} <span class="cf-req">*</span></label> + <input type="email" id="cf-email" name="email" required autocomplete="email" maxlength="254"> + </div> + </div> + + <div class="cf-field"> + <label for="cf-subject">{{ i18n "form_subject" }}</label> + <input type="text" id="cf-subject" name="subject" maxlength="200"> + </div> + + <div class="cf-field"> + <label for="cf-message">{{ i18n "form_message" }} <span class="cf-req">*</span></label> + <textarea id="cf-message" name="message" required maxlength="5000" rows="5"></textarea> + </div> + + <div class="cf-footer"> + <button type="submit" class="btn btn-primary cf-submit"> + <i class="fa-solid fa-paper-plane"></i> + <span>{{ i18n "form_send" }}</span> + </button> + <button type="reset" class="btn btn-outline cf-clear"> + {{ i18n "form_clear" }} + </button> + <p class="cf-status" aria-live="polite"></p> + </div> + </form> + </div> +</section> diff --git a/themes/danixme/layouts/partials/education.html b/themes/danixme/layouts/partials/education.html new file mode 100644 index 0000000..74b1374 --- /dev/null +++ b/themes/danixme/layouts/partials/education.html @@ -0,0 +1,28 @@ +<section id="education"> + <div class="container"> + <div class="section-eyebrow reveal">{{ i18n "section_education" }}</div> + <h2 class="section-title reveal">{{ i18n "education_title" }}</h2> + + <div class="edu-list"> + {{ $lang := .Site.Language.Lang }} + {{ range hugo.Data.education }} + {{ $current := index . "current" }} + <div class="edu-item reveal"> + <div class="edu-dot{{ if $current }} current{{ end }}"></div> + <div class="edu-body"> + <div class="edu-period">{{ .period }}</div> + <div class="edu-degree"> + {{ if eq $lang "it" }}{{ .degree_it }}{{ else }}{{ .degree_en }}{{ end }} + </div> + <div class="edu-institution"> + {{ if eq $lang "it" }}{{ .institution_it }}{{ else }}{{ .institution_en }}{{ end }} + </div> + <div class="edu-desc"> + {{ if eq $lang "it" }}{{ .desc_it | markdownify }}{{ else }}{{ .desc_en | markdownify }}{{ end }} + </div> + </div> + </div> + {{ end }} + </div> + </div> +</section> diff --git a/themes/danixme/layouts/partials/footer.html b/themes/danixme/layouts/partials/footer.html new file mode 100644 index 0000000..ad9d015 --- /dev/null +++ b/themes/danixme/layouts/partials/footer.html @@ -0,0 +1,22 @@ +<footer> + <div class="footer-social"> + {{ range hugo.Data.contact }} + {{ $eu := index . "email_user" }} + {{ $ed := index . "email_domain" }} + {{ if $eu }} + <a class="footer-icon" data-eu="{{ $eu }}" data-ed="{{ $ed }}" role="link" tabindex="0" title="{{ .label }}"> + <i class="{{ .icon }}"></i> + </a> + {{ else }} + <a href="{{ .href }}" class="footer-icon"{{ if .external }} target="_blank" rel="noopener"{{ end }} title="{{ .label }}"> + <i class="{{ .icon }}"></i> + </a> + {{ end }} + {{ end }} + </div> + + <div class="footer-top">{{ i18n "footer1" | safeHTML }}</div> + {{ with i18n "footer2" }}<div class="footer-top">{{ . | safeHTML }}</div>{{ end }} + + <div class="footer-copy">© {{ now.Year }} {{ .Site.Params.name }}</div> +</footer> diff --git a/themes/danixme/layouts/partials/head.html b/themes/danixme/layouts/partials/head.html new file mode 100644 index 0000000..ce87286 --- /dev/null +++ b/themes/danixme/layouts/partials/head.html @@ -0,0 +1,77 @@ +<head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <script>(function(){var s=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: light)').matches?'light':'dark');document.documentElement.classList.add('theme-'+s);})();</script> + + {{/* ── Primary Meta ──────────────────────────────── */}} + <title>{{ .Site.Title }}</title> + {{ $desc := .Params.description | default .Site.Params.description }} + <meta name="description" content="{{ $desc }}" /> + <meta name="author" content="{{ .Site.Params.name }}" /> + <meta name="robots" content="index, follow" /> + <link rel="canonical" href="{{ .Permalink }}" /> + + {{/* ── Hreflang (multilingual) ───────────────────── */}} + {{ range .Site.Home.AllTranslations }} + <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}" /> + {{ end }} + <link rel="alternate" hreflang="x-default" href="{{ .Site.Home.Permalink }}" /> + + {{/* ── Open Graph ───────────────────────────────── */}} + {{ $ogImage := .Params.og_image | default .Site.Params.og_image }} + <meta property="og:type" content="website" /> + <meta property="og:url" content="{{ .Permalink }}" /> + <meta property="og:title" content="{{ .Site.Title }}" /> + <meta property="og:description" content="{{ $desc }}" /> + {{ with $ogImage }}<meta property="og:image" content="{{ . | absURL }}" />{{ end }} + <meta property="og:locale" content="{{ .Site.Language.Params.og_locale | default .Site.Language.Lang }}" /> + {{ range .Site.Home.AllTranslations }}{{ if ne .Language.Lang $.Site.Language.Lang }} + <meta property="og:locale:alternate" content="{{ .Language.Params.og_locale | default .Language.Lang }}" /> + {{ end }}{{ end }} + + {{/* ── Twitter / X Card ────────────────────────── */}} + <meta name="twitter:card" content="summary_large_image" /> + <meta name="twitter:title" content="{{ .Site.Title }}" /> + <meta name="twitter:description" content="{{ $desc }}" /> + {{ with $ogImage }}<meta name="twitter:image" content="{{ . | absURL }}" />{{ end }} + + {{/* ── Geo (local SEO — Italy) ────────────────── */}} + <meta name="geo.region" content="IT" /> + <meta name="geo.placename" content="Italy" /> + + {{/* ── Favicon & icons ─────────────────────────── */}} + {{ with .Site.Params.favicon }}<link rel="icon" href="{{ . }}">{{ end }} + <link rel="apple-touch-icon" href="{{ .Site.Params.favicon | default "/img/fav.png" }}" /> + <meta name="theme-color" content="#06080b" media="(prefers-color-scheme: dark)" /> + <meta name="theme-color" content="#f0f4f8" media="(prefers-color-scheme: light)" /> + + {{/* ── JSON-LD: Person schema ───────────────────── */}} + {{- $photo := .Params.about_photo | default "/img/Danilo_profile.jpg" -}} + {{- $links := slice -}} + {{- range hugo.Data.contact -}}{{- with .href -}}{{- $links = $links | append . -}}{{- end -}}{{- end -}} + <script type="application/ld+json"> + { + "@context": "https://schema.org", + "@type": "Person", + "name": {{ .Site.Params.name | jsonify }}, + "url": {{ .Site.BaseURL | strings.TrimSuffix "/" | jsonify }}, + "image": {{ $photo | absURL | jsonify }}, + "jobTitle": {{ i18n "terminal_role" | jsonify }}, + "description": {{ $desc | jsonify }}, + "sameAs": [{{ range $i, $link := $links }}{{ if $i }},{{ end }}{{ $link | jsonify }}{{ end }}] + } + </script> + + {{/* ── Preconnect ───────────────────────────────── */}} + <link rel="preconnect" href="https://fonts.googleapis.com" /> + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> + <link rel="preconnect" href="https://cdnjs.cloudflare.com" /> + + {{/* ── Stylesheets ──────────────────────────────── */}} + <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,700;1,300&family=Oxanium:wght@700;800&display=swap" rel="stylesheet" /> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" integrity="sha384-nRgPTkuX86pH8yjPJUAFuASXQSSl2/bBUiNV47vSYpKFxHJhbcrGnmlYpYJMeD7a" crossorigin="anonymous" /> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.2.3/css/flag-icons.min.css" integrity="sha384-aQuvIWWIbpu/mSqULLDiUveyYiPoJzPKAWjUmGJ+Elm+N/LJhzfZqsutsfw870JS" crossorigin="anonymous" /> + {{ $css := resources.Get "css/main.css" }} + {{ if hugo.IsProduction }}{{ $css = $css | minify | fingerprint }}{{ end }} + <link rel="stylesheet" href="{{ $css.RelPermalink }}" /> +</head> diff --git a/themes/danixme/layouts/partials/hero.html b/themes/danixme/layouts/partials/hero.html new file mode 100644 index 0000000..aa5ab0f --- /dev/null +++ b/themes/danixme/layouts/partials/hero.html @@ -0,0 +1,60 @@ +<section id="hero"> + <canvas id="matrix-canvas" aria-hidden="true"></canvas> + + <div class="container"> + <div class="hero-inner"> + + <div class="hero-prompt">{{ i18n "hero_prompt" }}</div> + <h1 class="hero-name" data-text="{{ .Site.Params.name }}">{{ .Site.Params.name }}</h1> + + <div class="hero-bottom"> + <div class="hero-content"> + <div class="hero-role"> + <span id="typed"></span><span class="cursor"></span> + </div> + + <div class="hero-tagline">{{ .Params.tagline | markdownify }}</div> + + <div class="hero-cta"> + <a href="#contact" class="btn btn-primary">{{ i18n "btn_contact" }}</a> + <a href="#about" class="btn btn-outline">{{ i18n "btn_more" }}</a> + {{ with .Site.Language.Params.cv }} + <a class="btn btn-cv" data-cv="{{ . | base64Encode }}" role="button" tabindex="0"> + <i class="fa-solid fa-file-arrow-down"></i> + {{ i18n "btn_cv" }} + </a> + {{ end }} + </div> + </div> + + <div class="hero-term" aria-hidden="true"> + <div class="hero-term-bar"> + <span class="hero-term-dot" style="background:#ff5f57"></span> + <span class="hero-term-dot" style="background:#febc2e"></span> + <span class="hero-term-dot" style="background:#28c840"></span> + <span class="hero-term-title">root@{{ .Site.Params.handle }}</span> + </div> + <div class="hero-term-body"> + <div class="tl"><span class="tc-dim">$</span> connect <span class="tc-val">{{ .Site.Params.handle }}</span></div> + <div class="tl tl-d1"><span class="tc-dim">></span> establishing secure channel<span class="tc-blink">_</span></div> + <div class="tl tl-d2"><span class="tc-ok">✔</span> connected <span class="tc-dim">[TLS 1.3]</span></div> + <div class="tl tl-d3 tc-dim">──────────────────────────</div> + <div class="tl tl-d4"><span class="tc-key">USER </span><span class="tc-val">{{ .Site.Params.name }}</span></div> + <div class="tl tl-d5"><span class="tc-key">CERT </span><span class="tc-val">{{ .Site.Params.cert }}</span></div> + <div class="tl tl-d6"><span class="tc-key">FOCUS </span><span class="tc-val">{{ .Site.Params.focus }}</span></div> + <div class="tl tl-d7"><span class="tc-key">STATUS </span><span class="tc-ok">{{ i18n "terminal_status" }}</span></div> + <div class="tl tl-d8 tc-dim">──────────────────────────</div> + <div class="tl tl-d9"><span class="tc-dim">$</span> <span class="tc-blink">█</span></div> + </div> + </div> + + </div>{{/* end hero-bottom */}} + + </div>{{/* end hero-inner */}} + </div> + + <div class="scroll-indicator"> + {{ i18n "scroll" }} + <div class="scroll-line"></div> + </div> +</section> diff --git a/themes/danixme/layouts/partials/nav.html b/themes/danixme/layouts/partials/nav.html new file mode 100644 index 0000000..8a045d0 --- /dev/null +++ b/themes/danixme/layouts/partials/nav.html @@ -0,0 +1,45 @@ +<nav id="nav"> + <a href="#hero" class="nav-logo"> + {{- with .Site.Params.logo -}} + <img src="{{ . }}" alt="{{ $.Site.Params.handle }}" class="nav-logo-img"> + {{- end -}} + <span class="nav-logo-text">{{ .Site.Params.handle }}<span class="nav-logo-tld">.me</span></span> + </a> + + <div class="nav-right"> + <ul class="nav-links"> + <li><a href="#about">{{ i18n "nav_about" }}</a></li> + <li><a href="#services">{{ i18n "nav_services" }}</a></li> + <li><a href="#skills">{{ i18n "nav_skills" }}</a></li> + <li><a href="#education">{{ i18n "nav_education" }}</a></li> + <li><a href="#certifications">{{ i18n "nav_certs" }}</a></li> + <li><a href="#projects">{{ i18n "nav_projects" }}</a></li> + <li><a href="#contact">{{ i18n "nav_contact" }}</a></li> + <li class="nav-theme-item"> + <button class="theme-toggle" aria-label="Toggle theme"> + <i class="fa-solid fa-sun"></i> + <span class="theme-toggle-label"></span> + </button> + </li> + </ul> + + <button class="theme-toggle theme-toggle--desktop" aria-label="Toggle theme"> + <i class="fa-solid fa-sun"></i> + </button> + + <div class="lang-toggle" role="group" aria-label="Language"> + <span class="lang-btn active" title="{{ .Site.Language.LanguageName }}"> + <span class="fi fi-{{ .Site.Language.Params.flag }}"></span> + </span> + {{- range .Translations -}} + <a href="{{ .RelPermalink }}" class="lang-btn" title="{{ .Language.LanguageName }}"> + <span class="fi fi-{{ .Language.Params.flag }}"></span> + </a> + {{- end -}} + </div> + + <button class="hamburger" aria-label="Toggle menu" aria-expanded="false"> + <span></span><span></span><span></span> + </button> + </div> +</nav> diff --git a/themes/danixme/layouts/partials/projects.html b/themes/danixme/layouts/partials/projects.html new file mode 100644 index 0000000..d9bc7d4 --- /dev/null +++ b/themes/danixme/layouts/partials/projects.html @@ -0,0 +1,93 @@ +<section id="projects"> + <div class="container"> + <div class="section-eyebrow reveal">{{ i18n "section_projects" }}</div> + <h2 class="section-title reveal">{{ i18n "projects_title" }}</h2> + + {{ $lang := .Site.Language.Lang }} + {{ $count := add (len hugo.Data.projects) 1 }}{{/* +1 for the collab card */}} + {{ $wide := gt $count 3 }} + + <div class="carousel-wrap" data-carousel> + <div class="projects-grid is-carousel{{ if $wide }} is-wide-carousel{{ end }}"> + {{ range hugo.Data.projects }} + {{ $p := . }} + <div class="project-card reveal"> + {{ with .image }} + <div class="project-img-wrap"> + <img src="{{ . }}" alt="{{ $p.title }}" class="project-img"> + </div> + {{ end }} + + <div class="project-body"> + <h3 class="project-title">{{ .title }}</h3> + <p class="project-subtitle">{{ if eq $lang "it" }}{{ .subtitle_it }}{{ else }}{{ .subtitle_en }}{{ end }}</p> + + {{ with .tags }} + <div class="project-tags"> + {{ range . }}<span class="project-tag">{{ . }}</span>{{ end }} + </div> + {{ end }} + + <p class="project-desc">{{ if eq $lang "it" }}{{ .desc_it }}{{ else }}{{ .desc_en }}{{ end }}</p> + + <div class="project-footer"> + {{ with .url }} + <a href="{{ . }}" class="project-link" target="_blank" rel="noopener"> + <i class="fa-solid fa-arrow-up-right-from-square"></i> + {{ i18n "project_visit" }} + </a> + {{ end }} + + <div class="project-share"> + {{ $title := .title }} + {{ $url := .url }} + {{ with $url }} + <a href="https://www.linkedin.com/sharing/share-offsite/?url={{ $url | urlize }}" + class="share-btn" target="_blank" rel="noopener" aria-label="Share on LinkedIn"> + <i class="fa-brands fa-linkedin-in"></i> + </a> + <a href="https://x.com/intent/tweet?text={{ $title | urlize }}&url={{ $url | urlize }}" + class="share-btn" target="_blank" rel="noopener" aria-label="Share on X"> + <i class="fa-brands fa-x-twitter"></i> + </a> + <a href="mailto:?subject={{ $title | urlize }}&body={{ $url | urlize }}" + class="share-btn" aria-label="Share via Email"> + <i class="fa-solid fa-envelope"></i> + </a> + {{ end }} + </div> + </div> + </div> + </div> + {{ end }} + + {{/* ── Collaboration placeholder card ── */}} + <div class="project-card project-card--collab reveal"> + <div class="project-collab-icon"> + <i class="fa-solid fa-handshake-angle"></i> + </div> + <div class="project-body"> + <h3 class="project-title">{{ i18n "project_collab_title" }}</h3> + <p class="project-desc">{{ i18n "project_collab_desc" }}</p> + <div class="project-footer"> + <a href="#contact" class="project-link"> + <i class="fa-solid fa-paper-plane"></i> + {{ i18n "project_collab_cta" }} + </a> + </div> + </div> + </div> + + </div> + <div class="carousel-controls"> + <button class="carousel-btn carousel-btn--prev" aria-label="{{ i18n "carousel_prev" }}"> + <i class="fa-solid fa-chevron-left"></i> + </button> + <div class="carousel-dots"></div> + <button class="carousel-btn carousel-btn--next" aria-label="{{ i18n "carousel_next" }}"> + <i class="fa-solid fa-chevron-right"></i> + </button> + </div> + </div>{{/* end carousel-wrap */}} + </div> +</section> diff --git a/themes/danixme/layouts/partials/scripts.html b/themes/danixme/layouts/partials/scripts.html new file mode 100644 index 0000000..77e4583 --- /dev/null +++ b/themes/danixme/layouts/partials/scripts.html @@ -0,0 +1,11 @@ +<script> +window.SITE_PHRASES = {{ .Params.typed_phrases | jsonify | safeJS }}; +window.FORM_I18N = { + success: {{ i18n "form_success" | jsonify | safeJS }}, + error: {{ i18n "form_error" | jsonify | safeJS }}, + error_fields: {{ i18n "form_error_fields" | jsonify | safeJS }} +}; +</script> +{{ $js := resources.Get "js/main.js" }} +{{ if hugo.IsProduction }}{{ $js = $js | minify | fingerprint }}{{ end }} +<script src="{{ $js.RelPermalink }}"></script> diff --git a/themes/danixme/layouts/partials/services.html b/themes/danixme/layouts/partials/services.html new file mode 100644 index 0000000..538fb29 --- /dev/null +++ b/themes/danixme/layouts/partials/services.html @@ -0,0 +1,24 @@ +<section id="services"> + <div class="container"> + <div class="section-eyebrow reveal">{{ i18n "section_services" }}</div> + <h2 class="section-title reveal">{{ i18n "services_title" }}</h2> + + <div class="services-grid"> + {{ $lang := .Site.Language.Lang }} + {{ range hugo.Data.services }} + <div class="service-card reveal"> + <div class="service-icon"><i class="{{ .icon }}"></i></div> + <div class="service-title"> + {{ if eq $lang "it" }}{{ .title_it }}{{ else }}{{ .title_en }}{{ end }} + </div> + <div class="service-subtitle"> + {{ if eq $lang "it" }}{{ .subtitle_it }}{{ else }}{{ .subtitle_en }}{{ end }} + </div> + <div class="service-desc"> + {{ if eq $lang "it" }}{{ .desc_it | markdownify }}{{ else }}{{ .desc_en | markdownify }}{{ end }} + </div> + </div> + {{ end }} + </div> + </div> +</section> diff --git a/themes/danixme/layouts/partials/skills.html b/themes/danixme/layouts/partials/skills.html new file mode 100644 index 0000000..790bea3 --- /dev/null +++ b/themes/danixme/layouts/partials/skills.html @@ -0,0 +1,24 @@ +<section id="skills"> + <div class="container"> + <div class="section-eyebrow reveal">{{ i18n "section_skills" }}</div> + <h2 class="section-title reveal">{{ i18n "skills_title" }}</h2> + + <div class="skills-grid reveal" id="skillsGrid"> + {{ $lang := .Site.Language.Lang }} + {{ range hugo.Data.skills }} + <div class="skill-card"> + <div class="skill-head"> + <div class="skill-icon"><i class="{{ .icon }}"></i></div> + <div class="skill-name"> + {{ if eq $lang "it" }}{{ .name_it }}{{ else }}{{ .name_en }}{{ end }} + </div> + </div> + <div class="skill-track"><div class="skill-fill" style="--w:{{ .level }}"></div></div> + <div class="skill-tags"> + {{ range .tags }}<span class="tag">{{ . }}</span>{{ end }} + </div> + </div> + {{ end }} + </div> + </div> +</section> diff --git a/themes/danixme/layouts/robots.txt b/themes/danixme/layouts/robots.txt new file mode 100644 index 0000000..da0727f --- /dev/null +++ b/themes/danixme/layouts/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: {{ .Site.BaseURL }}sitemap.xml diff --git a/themes/danixme/theme.toml b/themes/danixme/theme.toml new file mode 100644 index 0000000..738686a --- /dev/null +++ b/themes/danixme/theme.toml @@ -0,0 +1,5 @@ +name = "danixme" +license = "MIT" +description = "Personal portfolio theme for danix.me" +tags = ["portfolio", "cybersecurity", "minimal"] +min_version = "0.120.0" |
