summaryrefslogtreecommitdiffstats
path: root/assets
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-04-10 11:29:00 +0200
committerDanilo M. <danix@danix.xyz>2026-04-10 11:29:00 +0200
commitc42150058196f5affad5c6c590e99dd2fc7321c3 (patch)
treecb0a7ad297128a43d32111e403959491573b6ace /assets
parentd51e4ef7dcd8609cd008a803f9d51674ac3d3ed2 (diff)
downloaddanixxyz-theme-c42150058196f5affad5c6c590e99dd2fc7321c3.tar.gz
danixxyz-theme-c42150058196f5affad5c6c590e99dd2fc7321c3.zip
feat: complete Hugo theme implementation from mockups
Transform all production-ready mockup files into a fully functional Hugo theme with all design patterns, components, and interactivity. Implements the complete plan: token alignment, global shell, homepage, articles section, single article views, photo gallery, static pages, and 404 page. Changes: - Phase 0: Token alignment (--color-* → --type-*, add spacing/z-index/timing scales) - Phase 1a: Global shell (baseof.html, hamburger menu, theme toggle, matrix rain) - Phase 1b: Homepage (hero layout, glitch/typing/scroll-reveal effects) - Phase 1c: Articles section (timeline layout, filter system, featured cards) - Phase 1d: Single article (meta bar, share sidebar, footer nav, progress bar) - Phase 1e: Photo gallery (lightbox, grid layout, shortcode updates) - Phase 1f: Static pages (about/contact page layout) - Phase 1g: 404 page (standalone HTML, quote randomization, recent articles) New files: - 6 CSS components: hamburger, article-hero, share-sidebar, timeline, lightbox, 404 - 8 JS modules: hamburger, glitch, typing, scroll-reveal, share-sidebar, lightbox, 404, photo-utils - 6 template partials: article-single, featured-card, photo-article, share-sidebar, static-page, timeline-item - 1 layout: 404.html (standalone) Updated: - All CSS variables with comprehensive token system - All JS modules integrated into main.js - All shortcodes (gallery, gal-img) for lightbox compatibility - All layout files (baseof, home, section, page) with new dispatching logic Verified: Hugo build succeeds with 21 pages, no errors. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Diffstat (limited to 'assets')
-rw-r--r--assets/css/components/404.css247
-rw-r--r--assets/css/components/article-hero.css225
-rw-r--r--assets/css/components/card.css30
-rw-r--r--assets/css/components/hamburger.css187
-rw-r--r--assets/css/components/hero.css306
-rw-r--r--assets/css/components/lightbox.css170
-rw-r--r--assets/css/components/progress-bar.css25
-rw-r--r--assets/css/components/share-sidebar.css107
-rw-r--r--assets/css/components/timeline.css336
-rw-r--r--assets/css/main.css22
-rw-r--r--assets/css/variables.css57
-rw-r--r--assets/js/404.js54
-rw-r--r--assets/js/filters.js45
-rw-r--r--assets/js/glitch.js30
-rw-r--r--assets/js/hamburger.js83
-rw-r--r--assets/js/lightbox.js31
-rw-r--r--assets/js/main.js13
-rw-r--r--assets/js/matrix-rain.js12
-rw-r--r--assets/js/photo-utils.js476
-rw-r--r--assets/js/progress-bar.js44
-rw-r--r--assets/js/scroll-reveal.js26
-rw-r--r--assets/js/share-sidebar.js51
-rw-r--r--assets/js/theme-toggle.js18
-rw-r--r--assets/js/typing.js69
24 files changed, 2521 insertions, 143 deletions
diff --git a/assets/css/components/404.css b/assets/css/components/404.css
new file mode 100644
index 0000000..d2c93b0
--- /dev/null
+++ b/assets/css/components/404.css
@@ -0,0 +1,247 @@
+/* 404.css */
+
+.hero--404 {
+ position: relative;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+}
+
+.content-wrapper {
+ display: grid;
+ grid-template-columns: 65% 35%;
+ gap: 3rem;
+ max-width: 1200px;
+ width: 100%;
+}
+
+@media (max-width: 900px) {
+ .content-wrapper {
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ }
+}
+
+/* Quote Section */
+.quote-section {
+ margin-bottom: 3rem;
+ padding: 2rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ position: relative;
+}
+
+.quote-mark {
+ font-size: 4rem;
+ color: var(--accent);
+ opacity: 0.3;
+ margin-bottom: -0.5rem;
+}
+
+.quote-text {
+ font-family: var(--font-body);
+ font-size: 1.3rem;
+ font-weight: 400;
+ line-height: 1.8;
+ color: var(--text);
+ margin-bottom: 1rem;
+}
+
+.quote-author {
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-dim);
+}
+
+/* Search Box */
+.search-box {
+ display: flex;
+ gap: 0;
+ margin-bottom: 3rem;
+ overflow: hidden;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+}
+
+.search-box input {
+ flex: 1;
+ padding: 0.75rem 1rem;
+ background: var(--surface);
+ border: none;
+ color: var(--text);
+ font-family: var(--font-mono);
+ font-size: 0.9rem;
+}
+
+.search-box input::placeholder {
+ color: var(--text-dim);
+}
+
+.search-box button {
+ padding: 0.75rem 1rem;
+ background: var(--accent);
+ border: none;
+ color: #000;
+ cursor: pointer;
+ transition: all var(--duration-base) ease;
+}
+
+.search-box button:hover {
+ background: var(--accent2);
+}
+
+/* Quick Nav */
+.quick-nav {
+ margin-bottom: 2rem;
+}
+
+.quick-nav h3 {
+ font-size: 1rem;
+ margin-bottom: 1rem;
+}
+
+.quick-nav ul {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.quick-nav a {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text);
+ text-decoration: none;
+ transition: all var(--duration-base) ease;
+}
+
+.quick-nav a:hover {
+ border-color: var(--accent);
+ background: rgba(168, 85, 247, 0.05);
+ padding-left: 1.25rem;
+}
+
+/* Right Column */
+.hero-right-404 {
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.recent-articles,
+.terminal-widget {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.recent-articles h3,
+.terminal-widget .terminal-bar {
+ padding: 1rem;
+ background: var(--bg2);
+ font-size: 0.9rem;
+ font-weight: 700;
+}
+
+.terminal-widget .terminal-bar {
+ display: flex;
+ gap: 0.75rem;
+ align-items: center;
+}
+
+.terminal-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ display: inline-block;
+}
+
+.recent-articles ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.recent-articles li {
+ border-bottom: 1px solid var(--border);
+ padding: 0;
+ margin: 0;
+}
+
+.recent-articles li:last-child {
+ border-bottom: none;
+}
+
+.article-link {
+ display: block;
+ padding: 0.75rem 1rem;
+ color: var(--text);
+ text-decoration: none;
+ font-size: 0.9rem;
+ transition: all var(--duration-base) ease;
+ border-left: 2px solid transparent;
+}
+
+.article-link:hover {
+ background: rgba(168, 85, 247, 0.05);
+ border-left-color: var(--accent);
+ padding-left: 1.25rem;
+}
+
+.article-link[data-type="tech"] {
+ color: var(--type-tech);
+}
+
+.article-link[data-type="life"] {
+ color: var(--type-life);
+}
+
+.article-link[data-type="quote"] {
+ color: var(--type-quote);
+}
+
+.article-link[data-type="link"] {
+ color: var(--type-link);
+}
+
+.article-link[data-type="photo"] {
+ color: var(--type-photo);
+}
+
+.terminal-content {
+ padding: 1rem;
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ color: var(--terminal-text, #c4d6e8);
+ line-height: 1.6;
+}
+
+.terminal-content div {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.terminal-prompt {
+ color: var(--terminal-prompt, #00ff88);
+}
+
+#terminal-files {
+ margin-top: 0.5rem;
+ color: var(--terminal-accent, #38bdf8);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .article-link:hover {
+ padding-left: 1rem;
+ }
+}
diff --git a/assets/css/components/article-hero.css b/assets/css/components/article-hero.css
new file mode 100644
index 0000000..cc9e180
--- /dev/null
+++ b/assets/css/components/article-hero.css
@@ -0,0 +1,225 @@
+/* article-hero.css */
+
+.article-hero {
+ position: relative;
+ min-height: 400px;
+ background-size: cover;
+ background-position: center;
+ display: flex;
+ align-items: flex-end;
+ padding: 3rem 2rem;
+ margin-bottom: 2rem;
+}
+
+.article-hero-overlay {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(180deg, rgba(6, 11, 16, 0.3) 0%, rgba(6, 11, 16, 0.8) 100%);
+}
+
+.article-hero-content {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ max-width: var(--container-max);
+ margin: 0 auto;
+ padding: 0 1.5rem;
+}
+
+.article-breadcrumb {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-family: var(--font-mono);
+ font-size: 0.85rem;
+ color: var(--text-dim);
+ margin-bottom: 1.5rem;
+}
+
+.article-breadcrumb a {
+ color: var(--accent);
+ text-decoration: none;
+ transition: color var(--duration-base) ease;
+}
+
+.article-breadcrumb a:hover {
+ color: var(--accent2);
+}
+
+.article-hero-content h1 {
+ font-size: clamp(2rem, 6vw, 3.5rem);
+ margin: 0;
+}
+
+.article-meta-bar {
+ position: sticky;
+ top: 0;
+ z-index: 50;
+ background: rgba(6, 11, 16, 0.95);
+ backdrop-filter: blur(4px);
+ padding: 1rem 2rem;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.article-meta {
+ display: flex;
+ align-items: center;
+ gap: 1.5rem;
+ font-size: 0.85rem;
+ font-family: var(--font-mono);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.article-type-badge {
+ padding: 0.4rem 0.8rem;
+ border-radius: 4px;
+ font-weight: 600;
+ background: color-mix(in srgb, currentColor 15%, transparent);
+ border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
+}
+
+.article-type-badge.type-tech {
+ color: var(--type-tech);
+}
+
+.article-type-badge.type-life {
+ color: var(--type-life);
+}
+
+.article-type-badge.type-quote {
+ color: var(--type-quote);
+}
+
+.article-type-badge.type-link {
+ color: var(--type-link);
+}
+
+.article-type-badge.type-photo {
+ color: var(--type-photo);
+}
+
+.article-date,
+.article-read-time {
+ color: var(--text-dim);
+}
+
+/* Article Body */
+.article-body {
+ padding: 2rem 1.5rem;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+/* Article Footer Nav */
+.article-footer-nav {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ padding: 2rem;
+ max-width: var(--container-max);
+ margin: 3rem auto 0;
+ border-top: 1px solid var(--border);
+}
+
+@media (max-width: 768px) {
+ .article-footer-nav {
+ grid-template-columns: 1fr;
+ }
+}
+
+.nav-prev,
+.nav-next {
+ padding: 1.5rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ text-decoration: none;
+ transition: all var(--duration-base) ease;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.nav-prev:hover,
+.nav-next:hover {
+ border-color: var(--accent);
+ background: rgba(168, 85, 247, 0.05);
+ transform: translateY(-2px);
+}
+
+.nav-label {
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--accent);
+}
+
+.nav-title {
+ font-family: var(--font-head);
+ font-size: 1.1rem;
+ font-weight: 700;
+ color: var(--text);
+}
+
+/* Static Page */
+.page-hero {
+ position: relative;
+ height: 40vh;
+ background: linear-gradient(135deg, var(--accent), var(--accent2));
+ display: flex;
+ align-items: flex-end;
+ padding: 3rem 2rem;
+ margin-bottom: 2rem;
+}
+
+.page-hero-overlay {
+ position: absolute;
+ inset: 0;
+ background: rgba(6, 11, 16, 0.3);
+}
+
+.page-hero-content {
+ position: relative;
+ z-index: 2;
+ width: 100%;
+ max-width: var(--container-max);
+ margin: 0 auto;
+ padding: 0 1.5rem;
+}
+
+.page-hero-content h1 {
+ font-size: clamp(2rem, 5vw, 3rem);
+ margin: 0;
+}
+
+.page-content {
+ padding: 2rem 1.5rem;
+}
+
+.page-nav ul {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.page-nav a {
+ padding: 0.75rem 1rem;
+ border-radius: 4px;
+ transition: all var(--duration-base) ease;
+ display: block;
+}
+
+.page-nav a:hover {
+ background: var(--surface);
+ padding-left: 1.25rem;
+}
+
+.page-nav a.active {
+ font-weight: 700;
+ color: var(--accent);
+}
diff --git a/assets/css/components/card.css b/assets/css/components/card.css
index f86c7bf..6a8cfaf 100644
--- a/assets/css/components/card.css
+++ b/assets/css/components/card.css
@@ -53,33 +53,33 @@
/* Type-specific badge colors */
.post-type-badge.tech {
- background: color-mix(in srgb, var(--color-tech) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--color-tech) 30%, transparent);
- color: var(--color-tech);
+ background: color-mix(in srgb, var(--type-tech) 15%, transparent);
+ border: 1px solid color-mix(in srgb, var(--type-tech) 30%, transparent);
+ color: var(--type-tech);
}
.post-type-badge.life {
- background: color-mix(in srgb, var(--color-life) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--color-life) 30%, transparent);
- color: var(--color-life);
+ background: color-mix(in srgb, var(--type-life) 15%, transparent);
+ border: 1px solid color-mix(in srgb, var(--type-life) 30%, transparent);
+ color: var(--type-life);
}
.post-type-badge.quote {
- background: color-mix(in srgb, var(--color-quote) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--color-quote) 30%, transparent);
- color: var(--color-quote);
+ background: color-mix(in srgb, var(--type-quote) 15%, transparent);
+ border: 1px solid color-mix(in srgb, var(--type-quote) 30%, transparent);
+ color: var(--type-quote);
}
.post-type-badge.link {
- background: color-mix(in srgb, var(--color-link) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--color-link) 30%, transparent);
- color: var(--color-link);
+ background: color-mix(in srgb, var(--type-link) 15%, transparent);
+ border: 1px solid color-mix(in srgb, var(--type-link) 30%, transparent);
+ color: var(--type-link);
}
.post-type-badge.photo {
- background: color-mix(in srgb, var(--color-photo) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--color-photo) 30%, transparent);
- color: var(--color-photo);
+ background: color-mix(in srgb, var(--type-photo) 15%, transparent);
+ border: 1px solid color-mix(in srgb, var(--type-photo) 30%, transparent);
+ color: var(--type-photo);
}
.post-card-title {
diff --git a/assets/css/components/hamburger.css b/assets/css/components/hamburger.css
new file mode 100644
index 0000000..200e81d
--- /dev/null
+++ b/assets/css/components/hamburger.css
@@ -0,0 +1,187 @@
+/* hamburger.css */
+
+.nav-header {
+ position: fixed;
+ top: 0;
+ right: 0;
+ z-index: var(--z-nav);
+ padding: var(--sp-6) var(--sp-8);
+}
+
+/* Hamburger Button */
+.hamburger {
+ background: none;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ gap: 0.4rem;
+ padding: 0.5rem;
+ transition: all var(--duration-base) ease;
+ width: 32px;
+ height: 32px;
+ align-items: center;
+ justify-content: center;
+}
+
+.hamburger span {
+ display: block;
+ width: 24px;
+ height: 2px;
+ background-color: var(--text);
+ border-radius: 2px;
+ transition: all var(--duration-base) ease;
+ transform-origin: center;
+}
+
+.hamburger.active span:nth-child(1) {
+ transform: translateY(10px) rotate(45deg);
+}
+
+.hamburger.active span:nth-child(2) {
+ opacity: 0;
+}
+
+.hamburger.active span:nth-child(3) {
+ transform: translateY(-10px) rotate(-45deg);
+}
+
+/* Menu Overlay */
+.menu-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(6, 11, 16, 0.95);
+ backdrop-filter: blur(4px);
+ z-index: var(--z-menu);
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity var(--duration-base) ease, visibility var(--duration-base) ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.menu-overlay.active {
+ opacity: 1;
+ visibility: visible;
+}
+
+/* Menu Items */
+.menu-items {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: var(--sp-12);
+ width: 90%;
+ max-width: 600px;
+ padding: var(--sp-8);
+}
+
+.menu-logo {
+ font-family: var(--font-head);
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: var(--text);
+ text-decoration: none;
+ margin-bottom: var(--sp-4);
+}
+
+.menu-links {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-6);
+ text-align: center;
+}
+
+.menu-links a {
+ color: var(--text);
+ text-decoration: none;
+ font-family: var(--font-mono);
+ font-size: 1.2rem;
+ padding: var(--sp-3) var(--sp-4);
+ border-radius: 4px;
+ transition: all var(--duration-base) ease;
+}
+
+.menu-links a:hover {
+ color: var(--accent);
+}
+
+.menu-links a[aria-current="page"] {
+ color: var(--accent);
+ font-weight: 700;
+}
+
+/* Menu Footer */
+.menu-footer {
+ display: flex;
+ justify-content: center;
+ margin-top: var(--sp-8);
+}
+
+/* Theme Switch Button */
+.theme-switch {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ cursor: pointer;
+ width: 50px;
+ height: 26px;
+ border-radius: 20px;
+ padding: 2px;
+ position: relative;
+ transition: all var(--duration-base) ease;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+}
+
+.theme-switch:hover {
+ border-color: var(--accent);
+}
+
+.theme-switch-dot {
+ width: 20px;
+ height: 20px;
+ background-color: var(--text);
+ border-radius: 50%;
+ display: block;
+ transition: transform var(--duration-base) ease;
+}
+
+.theme-switch.light {
+ justify-content: flex-end;
+}
+
+.theme-switch.light .theme-switch-dot {
+ transform: translateX(24px);
+}
+
+/* Keyboard Focus */
+.hamburger:focus-visible,
+.menu-links a:focus-visible,
+.theme-switch:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+/* Skip Link */
+.skip-link {
+ position: absolute;
+ top: -40px;
+ left: 0;
+ background: var(--accent);
+ color: var(--bg);
+ padding: var(--sp-2) var(--sp-4);
+ z-index: var(--z-modal);
+ text-decoration: none;
+ font-family: var(--font-mono);
+ font-size: 0.9rem;
+}
+
+.skip-link:focus {
+ top: 0;
+}
diff --git a/assets/css/components/hero.css b/assets/css/components/hero.css
index 567a3e9..3596274 100644
--- a/assets/css/components/hero.css
+++ b/assets/css/components/hero.css
@@ -1,82 +1,269 @@
/* hero.css */
+
.hero {
position: relative;
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ gap: 3rem;
overflow: hidden;
- padding: var(--section-py-mobile) 1.5rem;
- background: var(--bg);
- border-bottom: 1px solid var(--border);
-}
-
-@media (min-width: 768px) {
- .hero {
- padding: var(--section-py-desktop) 1.5rem;
- }
}
#matrix-canvas {
position: absolute;
inset: 0;
- opacity: 0.4;
+ opacity: 0.13;
pointer-events: none;
}
html.theme-light #matrix-canvas {
- opacity: 0.5;
+ opacity: 0.08;
}
-.hero-content {
+.hero-left {
+ flex: 1;
+ min-width: 0;
position: relative;
- z-index: 1;
- max-width: var(--container-max);
- margin: 0 auto;
- display: flex;
- align-items: center;
- gap: var(--gap-lg);
+ z-index: 2;
}
-.hero-avatar {
- width: 64px;
- height: 64px;
- border-radius: 50%;
- flex-shrink: 0;
- background: linear-gradient(135deg, var(--accent), var(--accent2));
- display: flex;
- align-items: center;
- justify-content: center;
+.hero-prompt {
+ font-size: 0.75rem;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--accent);
+ margin-bottom: 0.5rem;
+ font-weight: 500;
+ font-family: var(--font-mono);
+}
+
+.hero-name {
font-family: var(--font-head);
- font-size: 1.4rem;
+ font-size: clamp(3rem, 12vw, 7rem);
font-weight: 800;
- color: #fff;
+ letter-spacing: -0.04em;
+ line-height: 1;
+ margin-bottom: 1rem;
+ position: relative;
+ display: inline-block;
}
-@media (min-width: 768px) {
- .hero-avatar {
- width: 80px;
- height: 80px;
- font-size: 1.8rem;
- }
+/* Glitch effect on hero name */
+.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-text h1 {
- margin-bottom: 0.25rem;
+.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;
+}
+
+@keyframes glitch-red {
+ 0% { clip-path: inset(0 0 0 0); transform: translate(0); }
+ 20% { clip-path: inset(0 0 65% 0); transform: translate(-0.05em, -0.03em); }
+ 40% { clip-path: inset(28% 0 58% 0); transform: translate(0.05em, 0.03em); }
+ 60% { clip-path: inset(44% 0 58% 0); transform: translate(-0.05em, -0.02em); }
+ 80% { clip-path: inset(12% 0 85% 0); transform: translate(0.05em, 0.02em); }
+ 100% { clip-path: inset(0 0 0 0); transform: translate(0); }
+}
+
+@keyframes glitch-cyn {
+ 0% { clip-path: inset(0 0 0 0); transform: translate(0); }
+ 20% { clip-path: inset(0 0 60% 0); transform: translate(0.05em, 0.02em); }
+ 40% { clip-path: inset(38% 0 58% 0); transform: translate(-0.05em, 0.01em); }
+ 60% { clip-path: inset(19% 0 40% 0); transform: translate(0.025em, -0.02em); }
+ 80% { clip-path: inset(1% 0 58% 0); transform: translate(-0.05em, -0.02em); }
+ 100% { clip-path: inset(0 0 0 0); transform: translate(0); }
}
.hero-role {
- font-family: var(--font-mono);
- font-size: 0.85rem;
+ font-size: clamp(0.85rem, 3vw, 1rem);
+ letter-spacing: 0.05em;
+ margin-bottom: 1.5rem;
color: var(--accent);
+ font-weight: 400;
+ min-height: 1.5em;
+ font-family: var(--font-mono);
+}
+
+.cursor {
+ display: inline-block;
+ width: 0.15em;
+ height: 1em;
+ background: var(--accent);
+ margin-left: 0.1em;
+ animation: cursor-blink 1s step-end infinite;
+}
+
+@keyframes cursor-blink {
+ 0%, 49% { opacity: 1; }
+ 50%, 100% { opacity: 0; }
+}
+
+.hero-tagline {
+ font-family: var(--font-body);
+ font-size: 1rem;
+ font-weight: 400;
+ line-height: 1.95;
+ color: var(--text-dim);
+ margin-bottom: 2rem;
+ max-width: 90%;
+}
+
+/* Buttons */
+.hero-buttons {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+ margin-bottom: 2rem;
+}
+
+.btn {
+ padding: 0.75rem 1.5rem;
+ font-size: 0.8rem;
+ font-family: var(--font-mono);
letter-spacing: 0.1em;
text-transform: uppercase;
- margin-bottom: 0.75rem;
+ font-weight: 500;
+ cursor: pointer;
+ border: none;
+ transition: all var(--duration-base) ease;
+ text-decoration: none;
+ display: inline-block;
+ border-radius: 0;
}
-.hero-bio {
+.btn-primary {
+ background: var(--accent);
+ color: #000;
+ font-weight: 600;
+}
+
+.btn-primary:hover {
+ box-shadow: 0 0 24px rgba(168, 85, 247, 0.45);
+ transform: translate(0, -2px);
+}
+
+.btn-outline {
+ background: transparent;
+ color: var(--accent);
+ border: 1px solid var(--border);
+}
+
+.btn-outline:hover {
+ background: var(--accent);
+ color: #000;
+ box-shadow: 0 0 24px rgba(168, 85, 247, 0.45);
+}
+
+/* Right column: terminal widget */
+.hero-right {
+ flex: 0 0 auto;
+ width: 320px;
+ display: none;
+ position: relative;
+ z-index: 2;
+}
+
+@media (min-width: 1200px) {
+ .hero-right {
+ display: block;
+ }
+}
+
+.hero-terminal {
+ background: rgba(6, 11, 16, 0.85);
+ border: 1px solid rgba(168, 85, 247, 0.3);
+ border-radius: 8px;
+ overflow: hidden;
+ font-size: 0.75rem;
+ line-height: 1.7;
+}
+
+.terminal-bar {
+ background: var(--surface);
+ padding: 0.5rem 1rem;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.terminal-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ display: inline-block;
+}
+
+.terminal-content {
+ padding: 1rem;
+ font-family: var(--font-mono);
+ color: var(--terminal-text, #c4d6e8);
+}
+
+.terminal-content div {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+.terminal-prompt {
+ color: var(--terminal-prompt, #00ff88);
+}
+
+/* Scroll Indicator */
+.scroll-indicator {
+ position: absolute;
+ bottom: 2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.75rem;
color: var(--text-dim);
- font-size: 0.95rem;
- line-height: 1.8;
- max-width: 400px;
+ animation: bounce 2s infinite;
+ z-index: 2;
}
+.scroll-indicator svg {
+ color: var(--accent);
+}
+
+@keyframes bounce {
+ 0%, 100% { transform: translateX(-50%) translateY(0); }
+ 50% { transform: translateX(-50%) translateY(-8px); }
+}
/* Ambient glow behind hero */
.hero::before {
@@ -89,15 +276,38 @@ html.theme-light #matrix-canvas {
background: radial-gradient(circle, rgba(168, 85, 247, 0.15) 0%, transparent 70%);
transform: translate(-50%, -50%);
pointer-events: none;
+ z-index: 1;
}
-@media (max-width: 768px) {
- .hero-content {
+/* Mobile */
+@media (max-width: 900px) {
+ .hero {
flex-direction: column;
- text-align: center;
+ min-height: auto;
+ justify-content: flex-start;
+ padding-top: 6rem;
}
- .hero-bio {
+ .hero-tagline {
max-width: 100%;
}
+
+ .scroll-indicator {
+ display: none;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .hero-name.is-glitching::before,
+ .hero-name.is-glitching::after {
+ animation: none;
+ }
+
+ .scroll-indicator {
+ animation: none;
+ }
+
+ .cursor {
+ animation: none;
+ }
}
diff --git a/assets/css/components/lightbox.css b/assets/css/components/lightbox.css
new file mode 100644
index 0000000..ad34e84
--- /dev/null
+++ b/assets/css/components/lightbox.css
@@ -0,0 +1,170 @@
+/* lightbox.css */
+
+.photo-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: 1.5rem;
+ margin: 2rem 0;
+}
+
+.photo-card {
+ position: relative;
+ cursor: pointer;
+ overflow: hidden;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ transition: all var(--duration-base) ease;
+}
+
+.photo-card:hover {
+ border-color: var(--accent);
+ box-shadow: 0 0 20px rgba(168, 85, 247, 0.2);
+ transform: translateY(-4px);
+}
+
+.photo-card img {
+ width: 100%;
+ height: 200px;
+ object-fit: cover;
+ display: block;
+ transition: transform var(--duration-base) ease;
+}
+
+.photo-card:hover img {
+ transform: scale(1.05);
+}
+
+.photo-card figcaption {
+ padding: 1rem;
+ background: var(--surface);
+ font-size: 0.9rem;
+ color: var(--text-dim);
+}
+
+/* Lightbox Modal */
+.photo-lightbox {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ display: none;
+ background: rgba(6, 11, 16, 0.95);
+ backdrop-filter: blur(4px);
+}
+
+.photo-lightbox.active {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.photo-lightbox-backdrop {
+ position: absolute;
+ inset: 0;
+ cursor: pointer;
+}
+
+.photo-lightbox-content {
+ position: relative;
+ z-index: 201;
+ max-width: 90vw;
+ max-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.photo-lightbox-image {
+ max-width: 100%;
+ max-height: 70vh;
+ object-fit: contain;
+}
+
+.photo-lightbox-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background: var(--surface);
+ border-radius: 4px;
+}
+
+.photo-lightbox-nav {
+ display: flex;
+ gap: 1rem;
+}
+
+.photo-lightbox-nav button {
+ padding: 0.5rem 1rem;
+ background: var(--accent);
+ color: #000;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: all var(--duration-base) ease;
+}
+
+.photo-lightbox-nav button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.photo-lightbox-nav button:not(:disabled):hover {
+ box-shadow: 0 0 20px rgba(168, 85, 247, 0.4);
+ transform: translateY(-2px);
+}
+
+.photo-lightbox-close {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.5rem;
+ color: var(--text);
+ transition: all var(--duration-base) ease;
+ z-index: 202;
+}
+
+.photo-lightbox-close:hover {
+ background: var(--accent);
+ color: #000;
+ border-color: var(--accent);
+}
+
+.photo-metadata {
+ padding: 1rem;
+ background: var(--surface);
+ border-radius: 4px;
+ font-size: 0.9rem;
+ color: var(--text-dim);
+}
+
+.photo-metadata dt {
+ font-weight: 600;
+ color: var(--text);
+ margin-top: 0.5rem;
+}
+
+.photo-metadata dt:first-child {
+ margin-top: 0;
+}
+
+.photo-metadata dd {
+ margin-left: 1rem;
+ font-family: var(--font-mono);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .photo-card:hover img,
+ .photo-lightbox-nav button:hover {
+ transform: none;
+ }
+}
diff --git a/assets/css/components/progress-bar.css b/assets/css/components/progress-bar.css
index f89f4a8..ee17fcc 100644
--- a/assets/css/components/progress-bar.css
+++ b/assets/css/components/progress-bar.css
@@ -1,22 +1,19 @@
/* progress-bar.css */
-.reading-progress {
+
+.progress-bar {
position: fixed;
top: 0;
left: 0;
- height: 3px;
- background: linear-gradient(90deg, var(--accent), var(--accent2));
+ height: 2px;
+ background: linear-gradient(to right, var(--accent), var(--accent2));
+ box-shadow: 0 0 10px var(--accent-glow);
+ z-index: var(--z-progress);
width: 0%;
- z-index: 200;
- transition: width 0.1s ease-out;
-}
-
-/* Only show on pages with sufficient content */
-.article-page .reading-progress,
-.page-page .reading-progress {
- display: block;
+ transition: width var(--duration-base) ease;
}
-/* Hide if no scrollable content */
-body:not(.scrollable) .reading-progress {
- display: none;
+@media (prefers-reduced-motion: reduce) {
+ .progress-bar {
+ transition: none;
+ }
}
diff --git a/assets/css/components/share-sidebar.css b/assets/css/components/share-sidebar.css
new file mode 100644
index 0000000..8bc8d1d
--- /dev/null
+++ b/assets/css/components/share-sidebar.css
@@ -0,0 +1,107 @@
+/* share-sidebar.css */
+
+.share-sidebar {
+ position: fixed;
+ right: 2rem;
+ bottom: 50%;
+ transform: translateY(50%);
+ z-index: 30;
+ display: none;
+}
+
+@media (min-width: 1200px) {
+ .share-sidebar {
+ display: block;
+ }
+}
+
+.share-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.share-btn {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ color: var(--text);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all var(--duration-base) ease;
+ position: relative;
+}
+
+.share-btn:hover {
+ background: var(--accent);
+ border-color: var(--accent);
+ color: #000;
+ transform: scale(1.1);
+}
+
+.share-btn svg {
+ width: 20px;
+ height: 20px;
+}
+
+/* Tooltip */
+.share-btn::after {
+ content: attr(data-label);
+ position: absolute;
+ right: 100%;
+ top: 50%;
+ transform: translateY(-50%);
+ margin-right: 0.75rem;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ padding: 0.4rem 0.8rem;
+ border-radius: 4px;
+ white-space: nowrap;
+ font-size: 0.75rem;
+ color: var(--text-dim);
+ opacity: 0;
+ visibility: hidden;
+ transition: all var(--duration-base) ease;
+ pointer-events: none;
+}
+
+.share-btn:hover::after {
+ opacity: 1;
+ visibility: visible;
+ margin-right: 1rem;
+}
+
+/* Mobile: horizontal strip below article */
+@media (max-width: 1199px) {
+ .share-sidebar {
+ position: static;
+ transform: none;
+ display: flex;
+ justify-content: center;
+ margin: 2rem 0;
+ padding: 2rem 1.5rem;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ }
+
+ .share-buttons {
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .share-btn,
+ .share-btn::after {
+ transition: none;
+ }
+
+ .share-btn:hover {
+ transform: none;
+ }
+}
diff --git a/assets/css/components/timeline.css b/assets/css/components/timeline.css
new file mode 100644
index 0000000..c4a3678
--- /dev/null
+++ b/assets/css/components/timeline.css
@@ -0,0 +1,336 @@
+/* timeline.css */
+
+.page-header {
+ padding: 4rem 2rem 2rem;
+ max-width: var(--container-max);
+ margin: 0 auto;
+ text-align: center;
+}
+
+.page-header h1 {
+ margin-bottom: 2rem;
+}
+
+.filter-buttons {
+ display: flex;
+ justify-content: center;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.filter-btn {
+ padding: 0.6rem 1.2rem;
+ font-family: var(--font-mono);
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--text);
+ cursor: pointer;
+ border-radius: 4px;
+ transition: all var(--duration-base) ease;
+}
+
+.filter-btn:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.filter-btn.active {
+ background: var(--accent);
+ color: #000;
+ border-color: var(--accent);
+ font-weight: 600;
+}
+
+/* Featured Article */
+.featured-article {
+ max-width: var(--container-max);
+ margin: 3rem auto;
+ padding: 2rem;
+ background: var(--surface);
+ border: 2px solid transparent;
+ border-image: linear-gradient(135deg, var(--accent), var(--accent2)) 1;
+ border-radius: 8px;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ align-items: center;
+}
+
+@media (max-width: 768px) {
+ .featured-article {
+ grid-template-columns: 1fr;
+ }
+}
+
+.featured-image {
+ width: 100%;
+ overflow: hidden;
+ border-radius: 4px;
+}
+
+.featured-image img {
+ width: 100%;
+ height: 300px;
+ object-fit: cover;
+}
+
+.featured-body {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.featured-header {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+}
+
+.featured-type-badge {
+ font-weight: 600;
+}
+
+.featured-date {
+ color: var(--text-dim);
+}
+
+.featured-title {
+ font-size: clamp(1.5rem, 4vw, 2.5rem);
+ line-height: 1.2;
+ margin-bottom: 1rem;
+}
+
+.featured-excerpt {
+ color: var(--text-dim);
+ line-height: 1.8;
+ margin-bottom: 1rem;
+}
+
+.featured-link {
+ color: var(--accent);
+ text-decoration: none;
+ font-family: var(--font-mono);
+ font-size: 0.9rem;
+ transition: all var(--duration-base) ease;
+}
+
+.featured-link:hover {
+ color: var(--accent2);
+ transform: translateX(4px);
+}
+
+/* Timeline */
+.timeline-section {
+ position: relative;
+ max-width: var(--container-max);
+ margin: 4rem auto;
+ padding: 0 2rem;
+}
+
+.timeline-line {
+ position: absolute;
+ left: 50%;
+ top: 0;
+ bottom: 0;
+ width: 2px;
+ background: linear-gradient(to bottom, var(--accent), transparent);
+ transform: translateX(-50%);
+}
+
+@media (max-width: 900px) {
+ .timeline-line {
+ left: 0;
+ }
+}
+
+.timeline-feed {
+ position: relative;
+ padding: 2rem 0;
+}
+
+.timeline-item {
+ position: relative;
+ margin-bottom: 3rem;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ align-items: center;
+}
+
+.timeline-item.left {
+ grid-template-columns: 1fr 1fr;
+}
+
+.timeline-item.right {
+ grid-template-columns: 1fr 1fr;
+ direction: rtl;
+}
+
+.timeline-item.right > * {
+ direction: ltr;
+}
+
+@media (max-width: 900px) {
+ .timeline-item,
+ .timeline-item.left,
+ .timeline-item.right {
+ grid-template-columns: 1fr;
+ direction: ltr;
+ }
+
+ .timeline-item.right > * {
+ direction: ltr;
+ }
+}
+
+.timeline-dot {
+ position: absolute;
+ left: 50%;
+ top: 2rem;
+ width: 16px;
+ height: 16px;
+ background: var(--accent);
+ border: 3px solid var(--bg);
+ border-radius: 50%;
+ transform: translateX(-50%);
+ z-index: 2;
+}
+
+@media (max-width: 900px) {
+ .timeline-dot {
+ left: -8px;
+ }
+}
+
+.article-card {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ overflow: hidden;
+ transition: all var(--duration-base) ease;
+ cursor: pointer;
+}
+
+.article-card:hover {
+ border-color: var(--accent);
+ box-shadow: 0 0 20px rgba(168, 85, 247, 0.2);
+ transform: translateY(-4px);
+}
+
+.article-card-image {
+ width: 100%;
+ height: 200px;
+ overflow: hidden;
+}
+
+.article-card-image img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform var(--duration-base) ease;
+}
+
+.article-card:hover .article-card-image img {
+ transform: scale(1.05);
+}
+
+.article-card-body {
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.article-card-header {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+}
+
+.article-type-badge {
+ font-weight: 600;
+ padding: 0.3rem 0.6rem;
+ border-radius: 4px;
+ background: color-mix(in srgb, currentColor 15%, transparent);
+ border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
+}
+
+.article-type-badge.type-tech {
+ color: var(--type-tech);
+}
+
+.article-type-badge.type-life {
+ color: var(--type-life);
+}
+
+.article-type-badge.type-quote {
+ color: var(--type-quote);
+}
+
+.article-type-badge.type-link {
+ color: var(--type-link);
+}
+
+.article-type-badge.type-photo {
+ color: var(--type-photo);
+}
+
+.article-date {
+ color: var(--text-dim);
+}
+
+.article-card-title {
+ font-size: 1.3rem;
+ font-weight: 700;
+ line-height: 1.3;
+ margin: 0.5rem 0;
+}
+
+.article-card-title a {
+ color: var(--text);
+ text-decoration: none;
+ transition: color var(--duration-base) ease;
+}
+
+.article-card-title a:hover {
+ color: var(--accent);
+}
+
+.article-card-excerpt {
+ color: var(--text-dim);
+ font-size: 0.95rem;
+ line-height: 1.6;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+/* Hide/show articles based on filter */
+.timeline-item[data-type] {
+ display: none;
+}
+
+.timeline-item[data-type].visible,
+.timeline-item[data-filter="all"].visible {
+ display: grid;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .article-card:hover {
+ transform: none;
+ }
+
+ .article-card-image img {
+ transition: none;
+ }
+}
diff --git a/assets/css/main.css b/assets/css/main.css
index 518e749..ad78700 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -1,11 +1,17 @@
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;600&family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,700;1,300&family=Oxanium:wght@700;800&display=swap');
@import 'variables.css';
+@import 'components/hamburger.css';
@import 'components/header.css';
@import 'components/footer.css';
@import 'components/hero.css';
@import 'components/card.css';
@import 'components/feed.css';
+@import 'components/timeline.css';
+@import 'components/article-hero.css';
+@import 'components/share-sidebar.css';
+@import 'components/lightbox.css';
+@import 'components/404.css';
@import 'components/code.css';
@import 'components/progress-bar.css';
@@ -28,6 +34,22 @@ body {
font-size: var(--fs-body);
line-height: 1.95;
transition: background-color 0.2s, color 0.2s;
+ position: relative;
+}
+
+/* Dot grid background (shared across all pages) */
+body::before {
+ content: '';
+ position: fixed;
+ inset: 0;
+ z-index: var(--z-matrix);
+ pointer-events: none;
+ background-image: radial-gradient(circle, rgba(168, 85, 247, 0.07) 1px, transparent 1px);
+ background-size: 30px 30px;
+}
+
+html.theme-light body::before {
+ background-image: radial-gradient(circle, rgba(124, 58, 237, 0.05) 1px, transparent 1px);
}
/* Typography */
diff --git a/assets/css/variables.css b/assets/css/variables.css
index 74e4c86..371999a 100644
--- a/assets/css/variables.css
+++ b/assets/css/variables.css
@@ -8,16 +8,17 @@
--border: #182840;
--accent: #a855f7;
--accent2: #00ff88;
+ --accent-glow: rgba(168, 85, 247, 0.12);
--text: #c4d6e8;
--text-dim: #7a9bb8;
--muted: #304860;
- /* Type colors */
- --color-tech: #a855f7; /* purple */
- --color-life: #f59e0b; /* amber */
- --color-quote: #00ff88; /* green */
- --color-link: #38bdf8; /* cyan */
- --color-photo: #ec4899; /* pink */
+ /* Type colors (renamed from --color-*) */
+ --type-tech: #a855f7; /* purple */
+ --type-life: #f59e0b; /* amber */
+ --type-quote: #00ff88; /* green */
+ --type-link: #38bdf8; /* cyan */
+ --type-photo: #ec4899; /* pink */
/* Typography */
--font-body: 'IBM Plex Sans', system-ui, sans-serif;
@@ -40,13 +41,45 @@
--gap-lg: 2.5rem;
--gap-xl: 4rem;
- /* Spacing */
+ /* Spacing scale */
+ --sp-1: 0.25rem;
+ --sp-2: 0.5rem;
+ --sp-3: 0.75rem;
+ --sp-4: 1rem;
+ --sp-5: 1.25rem;
+ --sp-6: 1.5rem;
+ --sp-7: 1.75rem;
+ --sp-8: 2rem;
+ --sp-9: 2.25rem;
+ --sp-10: 2.5rem;
+ --sp-12: 3rem;
+ --sp-14: 3.5rem;
+ --sp-16: 4rem;
+
+ /* Z-index scale */
+ --z-base: 1;
+ --z-matrix: 0;
+ --z-nav: 100;
+ --z-menu: 99;
+ --z-modal: 200;
+ --z-progress: 9999;
+
+ /* Timing */
+ --duration-fast: 100ms;
+ --duration-base: 300ms;
+ --duration-slow: 500ms;
+
+ /* Easing */
+ --ease-out: cubic-bezier(0.33, 1, 0.68, 1);
+ --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
+
+ /* Section spacing */
--section-py-mobile: 4rem;
--section-py-desktop: 6rem;
--card-px-mobile: 1.5rem;
--card-px-desktop: 2rem;
- /* Transitions */
+ /* Transitions (legacy, keep for compatibility) */
--transition: all 0.2s ease;
--transition-slow: all 0.75s cubic-bezier(0.16,1,0.3,1);
}
@@ -59,9 +92,17 @@ html.theme-light {
--border: #a8bdd8;
--accent: #7c3aed;
--accent2: #008f5a;
+ --accent-glow: rgba(124, 58, 237, 0.08);
--text: #0d1b2a;
--text-dim: #2e4a6a;
--muted: #6888a8;
+
+ /* Type colors (light equivalents) */
+ --type-tech: #7c3aed; /* purple */
+ --type-life: #d97706; /* amber */
+ --type-quote: #008f5a; /* green */
+ --type-link: #0284c7; /* cyan */
+ --type-photo: #be185d; /* pink */
}
/* Breakpoints as CSS variables for reference */
diff --git a/assets/js/404.js b/assets/js/404.js
new file mode 100644
index 0000000..c26c218
--- /dev/null
+++ b/assets/js/404.js
@@ -0,0 +1,54 @@
+/**
+ * 404.js
+ * Quote randomization and terminal animation for 404 page
+ */
+
+(function() {
+ 'use strict';
+
+ const quotes = [
+ 'The page you are looking for doesn\'t exist. But that\'s okay, nothing exists until you find it.',
+ 'A 404 is just a redirect to a new beginning.',
+ 'You found a secret path. Sadly, it leads nowhere.',
+ 'This page chose to remain unknown.',
+ 'In the quantum realm, this page exists and doesn\'t exist simultaneously.',
+ 'Sometimes the best discoveries are the ones we never intended to find.',
+ ];
+
+ const quoteText = document.getElementById('quote-text');
+ const quoteAuthor = document.getElementById('quote-author');
+ const terminalFiles = document.getElementById('terminal-files');
+
+ if (quoteText && quoteAuthor) {
+ const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
+ quoteText.textContent = randomQuote;
+ quoteAuthor.textContent = '— 404 Philosopher';
+ }
+
+ if (terminalFiles) {
+ const files = [
+ 'post-01-security.md',
+ 'post-02-web-dev.md',
+ 'post-03-bash-tips.md',
+ 'about.md',
+ 'contact.md',
+ ];
+
+ files.forEach((file) => {
+ const line = document.createElement('div');
+ line.textContent = file;
+ terminalFiles.appendChild(line);
+ });
+ }
+
+ // Listen for search (if implemented)
+ const searchBtn = document.querySelector('.search-box button');
+ if (searchBtn) {
+ searchBtn.addEventListener('click', () => {
+ const input = document.querySelector('.search-box input');
+ if (input && input.value) {
+ window.location.href = `/articles/?search=${encodeURIComponent(input.value)}`;
+ }
+ });
+ }
+})();
diff --git a/assets/js/filters.js b/assets/js/filters.js
index 64d9c57..f7fa6a6 100644
--- a/assets/js/filters.js
+++ b/assets/js/filters.js
@@ -1,28 +1,37 @@
-// filters.js
+/**
+ * filters.js
+ * Article filtering by type on the articles page
+ */
+
(function() {
- const filterBtns = document.querySelectorAll('.filter-btn');
- const feedList = document.getElementById('articles-feed');
- const cards = feedList ? feedList.querySelectorAll('.post-card') : [];
+ 'use strict';
- if (!filterBtns.length || !cards.length) return;
+ const filterBtns = document.querySelectorAll('.filter-btn');
+ const timelineItems = document.querySelectorAll('.timeline-item');
- filterBtns.forEach(btn => {
- btn.addEventListener('click', function() {
- const filter = this.dataset.filter;
+ filterBtns.forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const filter = btn.getAttribute('data-filter');
// Update active button
- filterBtns.forEach(b => b.classList.remove('active'));
- this.classList.add('active');
+ filterBtns.forEach((b) => b.classList.remove('active'));
+ btn.classList.add('active');
- // Filter cards
- cards.forEach(card => {
- const cardType = card.querySelector('.post-type-badge')?.classList[1];
- const matches = filter === 'all' || cardType === filter;
- card.style.display = matches ? '' : 'none';
- });
+ // Filter articles
+ timelineItems.forEach((item) => {
+ const type = item.getAttribute('data-type');
- // Scroll to top
- window.scrollTo({ top: 0, behavior: 'smooth' });
+ if (filter === 'all' || type === filter) {
+ item.classList.add('visible');
+ } else {
+ item.classList.remove('visible');
+ }
+ });
});
});
+
+ // Show all on load
+ timelineItems.forEach((item) => {
+ item.classList.add('visible');
+ });
})();
diff --git a/assets/js/glitch.js b/assets/js/glitch.js
new file mode 100644
index 0000000..85f8a00
--- /dev/null
+++ b/assets/js/glitch.js
@@ -0,0 +1,30 @@
+/**
+ * glitch.js
+ * Random glitch effect on .hero-name every 4-11 seconds
+ */
+
+export function initGlitch() {
+ 'use strict';
+
+ const heroName = document.querySelector('.hero-name');
+ if (!heroName) return;
+
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+
+ function triggerGlitch() {
+ heroName.classList.add('is-glitching');
+ setTimeout(() => {
+ heroName.classList.remove('is-glitching');
+ }, 450);
+ }
+
+ function scheduleNextGlitch() {
+ const delay = Math.random() * 7000 + 4000; // 4-11 seconds
+ setTimeout(() => {
+ triggerGlitch();
+ scheduleNextGlitch();
+ }, delay);
+ }
+
+ scheduleNextGlitch();
+}
diff --git a/assets/js/hamburger.js b/assets/js/hamburger.js
new file mode 100644
index 0000000..1d27633
--- /dev/null
+++ b/assets/js/hamburger.js
@@ -0,0 +1,83 @@
+/**
+ * hamburger.js
+ * Hamburger menu toggle with focus trap and keyboard navigation
+ */
+
+export function initHamburger() {
+ 'use strict';
+
+ const hamburgerBtn = document.getElementById('hamburger-btn');
+ const menuOverlay = document.getElementById('menu-overlay');
+ const menuLinks = document.querySelectorAll('.menu-links a');
+ const themeSwitch = document.getElementById('theme-switch');
+
+ if (!hamburgerBtn || !menuOverlay) return;
+
+ // Toggle menu on hamburger click
+ hamburgerBtn.addEventListener('click', () => {
+ const isOpen = hamburgerBtn.getAttribute('aria-expanded') === 'true';
+ setMenuOpen(!isOpen);
+ });
+
+ // Close menu on overlay click
+ menuOverlay.addEventListener('click', (e) => {
+ if (e.target === menuOverlay) {
+ setMenuOpen(false);
+ }
+ });
+
+ // Close menu on menu link click
+ menuLinks.forEach((link) => {
+ link.addEventListener('click', () => {
+ setMenuOpen(false);
+ });
+ });
+
+ // Close menu on Escape key
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape') {
+ setMenuOpen(false);
+ }
+ });
+
+ // Focus trap: Tab through menu items and theme switch
+ menuOverlay.addEventListener('keydown', (e) => {
+ if (e.key !== 'Tab') return;
+
+ const focusableElements = Array.from(
+ menuOverlay.querySelectorAll('a, button')
+ );
+ const firstElement = focusableElements[0];
+ const lastElement = focusableElements[focusableElements.length - 1];
+
+ if (e.shiftKey) {
+ if (document.activeElement === firstElement) {
+ lastElement.focus();
+ e.preventDefault();
+ }
+ } else {
+ if (document.activeElement === lastElement) {
+ firstElement.focus();
+ e.preventDefault();
+ }
+ }
+ });
+
+ function setMenuOpen(isOpen) {
+ hamburgerBtn.setAttribute('aria-expanded', isOpen);
+ hamburgerBtn.classList.toggle('active', isOpen);
+ menuOverlay.classList.toggle('active', isOpen);
+
+ if (isOpen) {
+ document.body.style.overflow = 'hidden';
+ // Focus the first menu link
+ const firstLink = menuLinks[0];
+ if (firstLink) {
+ setTimeout(() => firstLink.focus(), 100);
+ }
+ } else {
+ document.body.style.overflow = '';
+ hamburgerBtn.focus();
+ }
+ }
+}
diff --git a/assets/js/lightbox.js b/assets/js/lightbox.js
new file mode 100644
index 0000000..81c3613
--- /dev/null
+++ b/assets/js/lightbox.js
@@ -0,0 +1,31 @@
+/**
+ * lightbox.js
+ * Photo lightbox initialization
+ */
+
+(function() {
+ 'use strict';
+
+ if (typeof PhotoUtils === 'undefined') return;
+
+ const photoGrid = document.querySelector('.photo-grid[data-lightbox="true"]');
+ if (!photoGrid) return;
+
+ const photoCards = photoGrid.querySelectorAll('.photo-card');
+ const photosData = [];
+
+ photoCards.forEach((card, index) => {
+ const figure = card.querySelector('figure');
+ const img = card.querySelector('img');
+ photosData.push({
+ index,
+ src: figure.getAttribute('data-src') || img.src,
+ alt: figure.getAttribute('data-alt') || img.alt,
+ caption: figure.getAttribute('data-caption'),
+ location: figure.getAttribute('data-location'),
+ });
+ });
+
+ // Initialize lightbox with PhotoUtils
+ PhotoUtils.initLightbox('.photo-grid', photosData);
+})();
diff --git a/assets/js/main.js b/assets/js/main.js
index 5b5848c..ee6b24f 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -3,3 +3,16 @@ import './theme-toggle.js';
import './matrix-rain.js';
import './progress-bar.js';
import './copy-code.js';
+import { initHamburger } from './hamburger.js';
+import { initGlitch } from './glitch.js';
+import { initTyping } from './typing.js';
+import { initScrollReveal } from './scroll-reveal.js';
+import { initShareSidebar } from './share-sidebar.js';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initHamburger();
+ initGlitch();
+ initTyping();
+ initScrollReveal();
+ initShareSidebar();
+});
diff --git a/assets/js/matrix-rain.js b/assets/js/matrix-rain.js
index 479231f..742b0bd 100644
--- a/assets/js/matrix-rain.js
+++ b/assets/js/matrix-rain.js
@@ -7,11 +7,19 @@
const ctx = canvas.getContext('2d');
const CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF<>/\\|{}[]$#@!';
const FS = 14; // font size / column width in px
+ const mode = canvas.getAttribute('data-mode') || 'background'; // 'hero' or 'background'
let cols, drops, raf;
function init() {
- canvas.width = canvas.offsetWidth;
- canvas.height = canvas.offsetHeight;
+ if (mode === 'hero') {
+ // Hero mode: size relative to canvas element's offsetWidth
+ canvas.width = canvas.offsetWidth;
+ canvas.height = canvas.offsetHeight;
+ } else {
+ // Background mode: size to full viewport
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ }
cols = Math.floor(canvas.width / FS) + 1;
drops = Array.from({ length: cols }, () => Math.random() * -(canvas.height / FS));
}
diff --git a/assets/js/photo-utils.js b/assets/js/photo-utils.js
new file mode 100644
index 0000000..32ede76
--- /dev/null
+++ b/assets/js/photo-utils.js
@@ -0,0 +1,476 @@
+(function() {
+ 'use strict';
+
+ // Internal state
+ let currentPhotoIndex = 0;
+ let photosData = [];
+ let isOpen = false;
+ let exifData = {};
+ let options = {
+ swipeEnabled: true,
+ keyboardEnabled: true,
+ extractEXIF: true,
+ onOpen: null,
+ onClose: null,
+ };
+
+ // Lightbox DOM elements (created on first init)
+ let lightboxModal = null;
+ let lightboxImage = null;
+ let lightboxCaption = null;
+ let lightboxSidebar = null;
+
+ /**
+ * Initialize lightbox for a photo collection
+ * @param {string} containerSelector - CSS selector for photo container
+ * @param {Array} photos - Array of photo objects: {src, alt, caption, location}
+ * @param {Object} opts - Options: {swipeEnabled, keyboardEnabled, extractEXIF, onOpen, onClose}
+ * @returns {Object} - {openLightbox, closeLightbox}
+ */
+ function initLightbox(containerSelector, photos, opts = {}) {
+ photosData = photos;
+ options = { ...options, ...opts };
+
+ // Merge options with defaults
+ const container = document.querySelector(containerSelector);
+ if (!container) {
+ console.error('Photo container not found:', containerSelector);
+ return { openLightbox: () => {}, closeLightbox: () => {} };
+ }
+
+ // Attach click listeners to all figures in the container
+ const figures = container.querySelectorAll('figure[data-photo-index]');
+ figures.forEach((figure) => {
+ figure.addEventListener('click', () => {
+ const index = parseInt(figure.dataset.photoIndex, 10);
+ openLightbox(index);
+ });
+ });
+
+ // Return public API
+ return {
+ openLightbox,
+ closeLightbox,
+ };
+ }
+
+ /**
+ * Open lightbox to a specific photo
+ * @param {number} photoIndex - 0-based index into photosData array
+ */
+ function openLightbox(photoIndex) {
+ if (photoIndex < 0 || photoIndex >= photosData.length) {
+ console.warn('Invalid photo index:', photoIndex);
+ return;
+ }
+
+ currentPhotoIndex = photoIndex;
+ isOpen = true;
+
+ // Create lightbox DOM if it doesn't exist
+ if (!lightboxModal) {
+ createLightboxDOM();
+ }
+
+ // Populate lightbox with current photo
+ updateLightboxContent();
+
+ // Show modal
+ lightboxModal.classList.add('visible');
+ document.body.style.overflow = 'hidden';
+
+ // Extract EXIF if enabled
+ if (options.extractEXIF) {
+ extractEXIFForCurrentPhoto();
+ }
+
+ // Attach event listeners
+ attachEventListeners();
+
+ // Callback
+ if (options.onOpen) {
+ options.onOpen(photoIndex);
+ }
+ }
+
+ /**
+ * Close lightbox
+ */
+ function closeLightbox() {
+ if (!isOpen || !lightboxModal) return;
+
+ isOpen = false;
+ lightboxModal.classList.remove('visible');
+ document.body.style.overflow = '';
+
+ // Detach event listeners
+ detachEventListeners();
+
+ // Callback
+ if (options.onClose) {
+ options.onClose();
+ }
+ }
+
+ /**
+ * Create lightbox HTML structure and inject into DOM
+ */
+ function createLightboxDOM() {
+ lightboxModal = document.createElement('div');
+ lightboxModal.className = 'photo-lightbox';
+
+ lightboxModal.innerHTML = `
+ <div class="photo-lightbox-backdrop"></div>
+ <div class="photo-lightbox-container">
+ <button class="photo-lightbox-close" aria-label="Close lightbox">×</button>
+
+ <button class="photo-lightbox-prev" aria-label="Previous photo">←</button>
+
+ <div class="photo-lightbox-content">
+ <img class="photo-lightbox-image" src="" alt="">
+ <div class="photo-lightbox-caption"></div>
+ </div>
+
+ <button class="photo-lightbox-next" aria-label="Next photo">→</button>
+
+ <div class="photo-lightbox-sidebar">
+ <div class="photo-lightbox-sidebar-content"></div>
+ </div>
+ </div>
+ `;
+
+ document.body.appendChild(lightboxModal);
+
+ // Cache element references
+ lightboxImage = lightboxModal.querySelector('.photo-lightbox-image');
+ lightboxCaption = lightboxModal.querySelector('.photo-lightbox-caption');
+ lightboxSidebar = lightboxModal.querySelector('.photo-lightbox-sidebar-content');
+
+ // Attach close button and backdrop click
+ lightboxModal.querySelector('.photo-lightbox-close').addEventListener('click', closeLightbox);
+ lightboxModal.querySelector('.photo-lightbox-backdrop').addEventListener('click', closeLightbox);
+
+ // Attach navigation buttons
+ lightboxModal.querySelector('.photo-lightbox-prev').addEventListener('click', () => navigate(-1));
+ lightboxModal.querySelector('.photo-lightbox-next').addEventListener('click', () => navigate(1));
+ }
+
+ /**
+ * Update lightbox content with current photo
+ */
+ function updateLightboxContent() {
+ const photo = photosData[currentPhotoIndex];
+ if (!photo) return;
+
+ lightboxImage.src = photo.src;
+ lightboxImage.alt = photo.alt || '';
+
+ // Caption
+ if (photo.caption) {
+ lightboxCaption.textContent = photo.caption;
+ lightboxCaption.style.display = 'block';
+ } else {
+ lightboxCaption.style.display = 'none';
+ }
+
+ // Update prev/next button states
+ const prevBtn = lightboxModal.querySelector('.photo-lightbox-prev');
+ const nextBtn = lightboxModal.querySelector('.photo-lightbox-next');
+ prevBtn.disabled = currentPhotoIndex === 0;
+ nextBtn.disabled = currentPhotoIndex === photosData.length - 1;
+ }
+
+ /**
+ * Navigate to previous or next photo
+ * @param {number} direction - -1 for prev, 1 for next
+ */
+ function navigate(direction) {
+ const newIndex = currentPhotoIndex + direction;
+ if (newIndex >= 0 && newIndex < photosData.length) {
+ currentPhotoIndex = newIndex;
+ updateLightboxContent();
+
+ // Reset EXIF data and re-extract if enabled
+ exifData = {};
+ if (options.extractEXIF) {
+ extractEXIFForCurrentPhoto();
+ }
+ }
+ }
+
+ /**
+ * Extract EXIF data from current photo image element
+ */
+ function extractEXIFForCurrentPhoto() {
+ extractEXIF(lightboxImage).then((data) => {
+ exifData = data;
+ updateSidebarContent();
+ });
+ }
+
+ /**
+ * Update sidebar with EXIF and location data
+ */
+ function updateSidebarContent() {
+ const photo = photosData[currentPhotoIndex];
+ let sidebarHTML = '';
+
+ // Always show location if provided
+ if (photo.location) {
+ sidebarHTML += `
+ <div class="photo-metadata-field">
+ <div class="photo-metadata-label">Location</div>
+ <div class="photo-metadata-value">${photo.location}</div>
+ </div>
+ `;
+ }
+
+ // Show EXIF fields if available
+ if (Object.keys(exifData).length > 0) {
+ sidebarHTML += `
+ <div class="photo-metadata-divider"></div>
+ <div class="photo-metadata-label">Camera</div>
+ `;
+
+ if (exifData.camera) {
+ sidebarHTML += `
+ <div class="photo-metadata-value">${exifData.camera}</div>
+ `;
+ }
+ if (exifData.lens) {
+ sidebarHTML += `
+ <div class="photo-metadata-field">
+ <div class="photo-metadata-label">Lens</div>
+ <div class="photo-metadata-value">${exifData.lens}</div>
+ </div>
+ `;
+ }
+ if (exifData.focalLength) {
+ sidebarHTML += `
+ <div class="photo-metadata-field">
+ <div class="photo-metadata-label">Focal Length</div>
+ <div class="photo-metadata-value">${exifData.focalLength}</div>
+ </div>
+ `;
+ }
+ if (exifData.aperture) {
+ sidebarHTML += `
+ <div class="photo-metadata-field">
+ <div class="photo-metadata-label">Aperture</div>
+ <div class="photo-metadata-value">${exifData.aperture}</div>
+ </div>
+ `;
+ }
+ if (exifData.shutterSpeed) {
+ sidebarHTML += `
+ <div class="photo-metadata-field">
+ <div class="photo-metadata-label">Shutter Speed</div>
+ <div class="photo-metadata-value">${exifData.shutterSpeed}</div>
+ </div>
+ `;
+ }
+ if (exifData.iso) {
+ sidebarHTML += `
+ <div class="photo-metadata-field">
+ <div class="photo-metadata-label">ISO</div>
+ <div class="photo-metadata-value">${exifData.iso}</div>
+ </div>
+ `;
+ }
+ if (exifData.dateTaken) {
+ sidebarHTML += `
+ <div class="photo-metadata-field">
+ <div class="photo-metadata-label">Date Taken</div>
+ <div class="photo-metadata-value">${exifData.dateTaken}</div>
+ </div>
+ `;
+ }
+ }
+
+ // Show or hide sidebar based on whether there's content
+ const sidebarContainer = lightboxModal.querySelector('.photo-lightbox-sidebar');
+ if (sidebarHTML) {
+ lightboxSidebar.innerHTML = sidebarHTML;
+ sidebarContainer.classList.add('visible');
+ } else {
+ sidebarContainer.classList.remove('visible');
+ }
+ }
+
+ /**
+ * Extract EXIF data from an image element using exif-js library
+ * @param {HTMLImageElement} imageElement - Image to read EXIF from
+ * @returns {Promise} - Resolves to {camera, lens, iso, aperture, shutterSpeed, focalLength, dateTaken}
+ */
+ function extractEXIF(imageElement) {
+ return new Promise((resolve) => {
+ // If exif-js is not loaded, return empty object
+ if (typeof EXIF === 'undefined') {
+ console.warn('exif-js library not loaded');
+ resolve({});
+ return;
+ }
+
+ EXIF.getData(imageElement, function() {
+ const data = {
+ camera: EXIF.getTag(this, 'Model'),
+ lens: EXIF.getTag(this, 'LensModel'),
+ iso: EXIF.getTag(this, 'ISOSpeedRatings'),
+ aperture: formatAperture(EXIF.getTag(this, 'FNumber')),
+ shutterSpeed: formatShutterSpeed(EXIF.getTag(this, 'ExposureTime')),
+ focalLength: formatFocalLength(EXIF.getTag(this, 'FocalLength')),
+ dateTaken: EXIF.getTag(this, 'DateTime'),
+ };
+
+ // Filter out undefined values
+ Object.keys(data).forEach(key => {
+ if (data[key] === undefined) {
+ delete data[key];
+ }
+ });
+
+ resolve(data);
+ });
+ });
+ }
+
+ /**
+ * Format aperture value (f-number)
+ */
+ function formatAperture(value) {
+ if (!value) return null;
+ if (typeof value === 'object' && value.numerator !== undefined) {
+ return `f/${(value.numerator / value.denominator).toFixed(1)}`;
+ }
+ return `f/${value}`;
+ }
+
+ /**
+ * Format shutter speed (exposure time)
+ */
+ function formatShutterSpeed(value) {
+ if (!value) return null;
+ if (typeof value === 'object' && value.numerator !== undefined) {
+ const speed = value.numerator / value.denominator;
+ if (speed >= 1) {
+ return `${speed.toFixed(1)}s`;
+ }
+ return `1/${Math.round(1 / speed)}`;
+ }
+ return value;
+ }
+
+ /**
+ * Format focal length
+ */
+ function formatFocalLength(value) {
+ if (!value) return null;
+ if (typeof value === 'object' && value.numerator !== undefined) {
+ return `${(value.numerator / value.denominator).toFixed(0)}mm`;
+ }
+ return `${value}mm`;
+ }
+
+ /**
+ * Attach event listeners (swipe, keyboard, etc.)
+ */
+ function attachEventListeners() {
+ if (options.swipeEnabled) {
+ attachSwipeListeners();
+ }
+ if (options.keyboardEnabled) {
+ attachKeyboardListeners();
+ }
+ }
+
+ /**
+ * Detach event listeners to prevent memory leaks
+ */
+ function detachEventListeners() {
+ if (options.swipeEnabled) {
+ detachSwipeListeners();
+ }
+ if (options.keyboardEnabled) {
+ detachKeyboardListeners();
+ }
+ }
+
+ /**
+ * Touch swipe event listeners
+ */
+ let swipeStartX = 0;
+ let swipeStartY = 0;
+
+ function onTouchStart(e) {
+ swipeStartX = e.touches[0].clientX;
+ swipeStartY = e.touches[0].clientY;
+ }
+
+ function onTouchEnd(e) {
+ const swipeEndX = e.changedTouches[0].clientX;
+ const swipeEndY = e.changedTouches[0].clientY;
+ const diffX = swipeEndX - swipeStartX;
+ const diffY = swipeEndY - swipeStartY;
+
+ // Only consider horizontal swipe if delta-x > delta-y
+ if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
+ if (diffX > 0) {
+ // Swiped right: go to previous
+ navigate(-1);
+ } else {
+ // Swiped left: go to next
+ navigate(1);
+ }
+ }
+ }
+
+ function attachSwipeListeners() {
+ lightboxModal.addEventListener('touchstart', onTouchStart, false);
+ lightboxModal.addEventListener('touchend', onTouchEnd, false);
+ }
+
+ function detachSwipeListeners() {
+ lightboxModal.removeEventListener('touchstart', onTouchStart, false);
+ lightboxModal.removeEventListener('touchend', onTouchEnd, false);
+ }
+
+ /**
+ * Keyboard event listeners
+ */
+ function onKeyDown(e) {
+ if (!isOpen) return;
+
+ switch (e.key) {
+ case 'ArrowLeft':
+ navigate(-1);
+ e.preventDefault();
+ break;
+ case 'ArrowRight':
+ navigate(1);
+ e.preventDefault();
+ break;
+ case 'Escape':
+ closeLightbox();
+ e.preventDefault();
+ break;
+ default:
+ break;
+ }
+ }
+
+ function attachKeyboardListeners() {
+ document.addEventListener('keydown', onKeyDown);
+ }
+
+ function detachKeyboardListeners() {
+ document.removeEventListener('keydown', onKeyDown);
+ }
+
+ // Expose public API
+ window.PhotoUtils = {
+ initLightbox,
+ extractEXIF,
+ openLightbox,
+ closeLightbox,
+ };
+})();
diff --git a/assets/js/progress-bar.js b/assets/js/progress-bar.js
index bc8b70a..e171f4f 100644
--- a/assets/js/progress-bar.js
+++ b/assets/js/progress-bar.js
@@ -1,38 +1,20 @@
-// progress-bar.js
+/**
+ * progress-bar.js
+ * Reading progress indicator for articles
+ */
+
(function() {
- const progressBar = document.querySelector('.reading-progress');
- if (!progressBar) return;
+ 'use strict';
- // Only enable on pages with substantial content
- const mainContent = document.querySelector('main');
- if (!mainContent) return;
+ const progressBar = document.getElementById('progress-bar');
+ if (!progressBar) return;
- function updateProgress() {
- // Calculate scroll percentage
- const windowHeight = window.innerHeight;
- const docHeight = document.documentElement.scrollHeight - windowHeight;
+ window.addEventListener('scroll', () => {
+ const windowHeight = document.documentElement.scrollHeight - window.innerHeight;
const scrolled = window.scrollY;
- const percent = docHeight > 0 ? (scrolled / docHeight) * 100 : 0;
+ const progress = windowHeight > 0 ? (scrolled / windowHeight) * 100 : 0;
- progressBar.style.width = percent + '%';
- }
-
- // Mark body as scrollable if there's significant content
- const contentHeight = mainContent.offsetHeight;
- if (contentHeight > window.innerHeight * 1.5) {
- document.body.classList.add('scrollable');
- }
-
- // Use requestAnimationFrame for smooth updates
- let ticking = false;
- window.addEventListener('scroll', function() {
- if (!ticking) {
- requestAnimationFrame(updateProgress);
- ticking = true;
- setTimeout(() => { ticking = false; }, 100);
- }
+ progressBar.style.width = progress + '%';
+ progressBar.setAttribute('aria-valuenow', Math.round(progress));
}, { passive: true });
-
- // Initial update
- updateProgress();
})();
diff --git a/assets/js/scroll-reveal.js b/assets/js/scroll-reveal.js
new file mode 100644
index 0000000..ab099c0
--- /dev/null
+++ b/assets/js/scroll-reveal.js
@@ -0,0 +1,26 @@
+/**
+ * scroll-reveal.js
+ * IntersectionObserver for revealing elements on scroll
+ */
+
+export function initScrollReveal() {
+ 'use strict';
+
+ const revealElements = document.querySelectorAll('.reveal');
+ if (!revealElements.length) return;
+
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ entry.target.classList.add('revealed');
+ observer.unobserve(entry.target);
+ }
+ });
+ }, {
+ threshold: 0.1,
+ });
+
+ revealElements.forEach((el) => {
+ observer.observe(el);
+ });
+}
diff --git a/assets/js/share-sidebar.js b/assets/js/share-sidebar.js
new file mode 100644
index 0000000..81e5f6c
--- /dev/null
+++ b/assets/js/share-sidebar.js
@@ -0,0 +1,51 @@
+/**
+ * share-sidebar.js
+ * Social sharing sidebar with copy-to-clipboard
+ */
+
+export function initShareSidebar() {
+ 'use strict';
+
+ const sidebar = document.getElementById('share-sidebar');
+ const copyBtn = document.getElementById('share-copy');
+
+ if (!sidebar) return;
+
+ // Share button handlers
+ const shareBtns = sidebar.querySelectorAll('.share-btn[data-platform]');
+ shareBtns.forEach((btn) => {
+ btn.addEventListener('click', () => {
+ const platform = btn.getAttribute('data-platform');
+ const url = sidebar.getAttribute('data-url');
+ const title = sidebar.getAttribute('data-title');
+
+ const shareUrls = {
+ whatsapp: `https://wa.me/?text=${encodeURIComponent(title + ' ' + url)}`,
+ telegram: `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
+ linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
+ twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
+ facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
+ reddit: `https://reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`,
+ email: `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(url)}`,
+ };
+
+ if (shareUrls[platform]) {
+ window.open(shareUrls[platform], '_blank');
+ }
+ });
+ });
+
+ // Copy to clipboard
+ if (copyBtn) {
+ copyBtn.addEventListener('click', () => {
+ const url = sidebar.getAttribute('data-url');
+ navigator.clipboard.writeText(url).then(() => {
+ const originalText = copyBtn.innerHTML;
+ copyBtn.innerHTML = '✓';
+ setTimeout(() => {
+ copyBtn.innerHTML = originalText;
+ }, 2000);
+ });
+ });
+ }
+}
diff --git a/assets/js/theme-toggle.js b/assets/js/theme-toggle.js
index 9f0fd5a..e03fce7 100644
--- a/assets/js/theme-toggle.js
+++ b/assets/js/theme-toggle.js
@@ -45,21 +45,25 @@
// Setup toggle button
function setupToggleButton() {
- const btn = document.getElementById('theme-toggle-btn');
+ const btn = document.getElementById('theme-switch');
if (btn) {
btn.addEventListener('click', toggleTheme);
- updateToggleButtonLabel();
+ updateToggleButtonUI();
- // Listen for theme changes to update button label
- window.addEventListener('theme-changed', updateToggleButtonLabel);
+ // Listen for theme changes to update button UI
+ window.addEventListener('theme-changed', updateToggleButtonUI);
}
}
- function updateToggleButtonLabel() {
- const btn = document.getElementById('theme-toggle-btn');
+ function updateToggleButtonUI() {
+ const btn = document.getElementById('theme-switch');
if (btn) {
const current = getCurrentTheme();
- btn.textContent = current === 'dark' ? '☀️ light' : '🌙 dark';
+ if (current === 'light') {
+ btn.classList.add('light');
+ } else {
+ btn.classList.remove('light');
+ }
}
}
diff --git a/assets/js/typing.js b/assets/js/typing.js
new file mode 100644
index 0000000..369fed7
--- /dev/null
+++ b/assets/js/typing.js
@@ -0,0 +1,69 @@
+/**
+ * typing.js
+ * Typing animation for .hero-role / #typed element
+ */
+
+export function initTyping() {
+ 'use strict';
+
+ const typedElement = document.getElementById('typed');
+ if (!typedElement) return;
+
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
+ const phrasesJson = typedElement.getAttribute('data-phrases');
+ let phrases = [];
+
+ if (phrasesJson) {
+ try {
+ phrases = JSON.parse(phrasesJson);
+ } catch (e) {
+ console.warn('Failed to parse typing phrases:', e);
+ phrases = ['Security & Web Dev', 'WordPress Developer'];
+ }
+ }
+
+ if (!phrases.length) return;
+
+ let currentPhraseIndex = 0;
+ let currentCharIndex = 0;
+ let isDeleting = false;
+
+ function type() {
+ const phrase = phrases[currentPhraseIndex];
+ const speed = isDeleting ? 50 : 100;
+
+ if (isDeleting) {
+ currentCharIndex--;
+ } else {
+ currentCharIndex++;
+ }
+
+ typedElement.textContent = phrase.substring(0, currentCharIndex);
+
+ // Add cursor
+ if (!isDeleting && currentCharIndex === phrase.length) {
+ typedElement.innerHTML += '<span class="cursor"></span>';
+ } else if (isDeleting && currentCharIndex === 0) {
+ currentPhraseIndex = (currentPhraseIndex + 1) % phrases.length;
+ isDeleting = false;
+ setTimeout(type, 500);
+ return;
+ } else {
+ typedElement.innerHTML = phrase.substring(0, currentCharIndex) + '<span class="cursor"></span>';
+ }
+
+ if (!isDeleting && currentCharIndex === phrase.length) {
+ isDeleting = true;
+ setTimeout(type, 2000);
+ } else {
+ setTimeout(type, prefersReducedMotion ? 0 : speed);
+ }
+ }
+
+ if (prefersReducedMotion) {
+ // Just show the first phrase without animation
+ typedElement.textContent = phrases[0];
+ } else {
+ type();
+ }
+}