Downloaded theme

This commit is contained in:
2026-03-23 18:35:59 +01:00
parent 326fab0e42
commit d091efd432
86 changed files with 14512 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
{{ define "main" }}
<section class="layout-page">
<div class="error-page">
<div class="error-content">
<!-- Error Code -->
<div class="error-code">404</div>
<!-- Error Message -->
<h1 class="error-title">Page Not Found</h1>
<p class="error-description">
The page you're looking for doesn't exist or has been moved.
</p>
<!-- Search Suggestion -->
<div class="error-search">
<p class="error-hint">
Try searching for what you need, or return to the homepage.
</p>
<div class="error-actions">
<button
onclick="window.MinimalSearch.open()"
class="btn-primary"
aria-label="Search site"
>
<i class="fa-solid fa-magnifying-glass"></i>
<span>Search Site</span>
</button>
<a href="/" class="btn-ghost">
<i class="fa-solid fa-house"></i>
<span>Go Home</span>
</a>
</div>
</div>
<!-- Helpful Links -->
<div class="error-links">
<h2 class="error-links-title">You might be interested in:</h2>
<div class="error-links-grid">
{{ with .Site.GetPage "/blog" }}
<a href="{{ .Permalink }}" class="error-link-card">
<i class="fa-regular fa-note-sticky"></i>
<div>
<strong>Blog</strong>
<span>Read latest articles</span>
</div>
</a>
{{ end }}
{{ with .Site.GetPage "/projects" }}
<a href="{{ .Permalink }}" class="error-link-card">
<i class="fa-regular fa-folder-open"></i>
<div>
<strong>Projects</strong>
<span>Explore my work</span>
</div>
</a>
{{ end }}
{{ with .Site.GetPage "/about" }}
<a href="{{ .Permalink }}" class="error-link-card">
<i class="fa-regular fa-user"></i>
<div>
<strong>About</strong>
<span>Learn more about me</span>
</div>
</a>
{{ end }}
</div>
</div>
<!-- Alternative: Show Recent Posts -->
{{ $recentPages := where .Site.RegularPages "Section" "in" (slice "blog" "projects") }}
{{ $recentPages = first 3 $recentPages }}
{{ if $recentPages }}
<div class="error-recent">
<h2 class="error-recent-title">Recent Content</h2>
<ul class="error-recent-list">
{{ range $recentPages }}
<li>
<a href="{{ .Permalink }}" class="error-recent-link">
<span class="error-recent-icon">
{{ if eq .Section "blog" }}
<i class="fa-regular fa-note-sticky"></i>
{{ else if eq .Section "projects" }}
<i class="fa-regular fa-folder-open"></i>
{{ else }}
<i class="fa-regular fa-file"></i>
{{ end }}
</span>
<span class="error-recent-text">
<strong>{{ .Title }}</strong>
{{ with .Description }}
<small>{{ . }}</small>
{{ end }}
</span>
</a>
</li>
{{ end }}
</ul>
</div>
{{ end }}
</div>
</div>
</section>
<style>
.error-page {
min-height: 60vh;
display: flex;
align-items: center;
justify-content: center;
padding: 4rem 0;
}
.error-content {
text-align: center;
max-width: 42rem;
margin: 0 auto;
}
.error-code {
font-size: 8rem;
font-weight: 700;
line-height: 1;
color: var(--color-accent);
margin-bottom: 1rem;
opacity: 0.9;
}
.error-title {
font-size: 2rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
}
.error-description {
font-size: 1.125rem;
color: var(--color-text-muted);
margin-bottom: 2rem;
}
.error-search {
margin-bottom: 3rem;
}
.error-hint {
color: var(--color-text-muted);
margin-bottom: 1.5rem;
}
.error-actions {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.error-links {
margin-top: 3rem;
padding-top: 3rem;
border-top: 1px solid var(--color-border);
}
.error-links-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1.5rem;
}
.error-links-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.error-link-card {
display: flex;
align-items: center;
gap: 1rem;
padding: 1.25rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
text-decoration: none;
color: var(--color-text);
transition: all 0.2s ease;
}
.error-link-card:hover {
border-color: var(--color-accent);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.error-link-card i {
font-size: 1.5rem;
color: var(--color-accent);
}
.error-link-card strong {
display: block;
font-weight: 600;
margin-bottom: 0.25rem;
}
.error-link-card span {
display: block;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.error-recent {
margin-top: 2rem;
}
.error-recent-title {
font-size: 1.125rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
text-align: left;
}
.error-recent-list {
list-style: none;
padding: 0;
margin: 0;
}
.error-recent-list li {
margin-bottom: 0.75rem;
}
.error-recent-link {
display: flex;
align-items: flex-start;
gap: 1rem;
padding: 1rem;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
text-decoration: none;
color: var(--color-text);
transition: all 0.2s ease;
}
.error-recent-link:hover {
border-color: var(--color-accent);
background: color-mix(in srgb, var(--color-surface) 95%, var(--color-accent));
}
.error-recent-icon {
flex-shrink: 0;
color: var(--color-accent);
font-size: 1.25rem;
padding-top: 0.25rem;
}
.error-recent-text {
flex: 1;
text-align: left;
}
.error-recent-text strong {
display: block;
font-weight: 600;
margin-bottom: 0.25rem;
}
.error-recent-text small {
display: block;
font-size: 0.875rem;
color: var(--color-text-muted);
line-height: 1.4;
}
@media (max-width: 640px) {
.error-code {
font-size: 5rem;
}
.error-title {
font-size: 1.5rem;
}
.error-description {
font-size: 1rem;
}
.error-actions {
flex-direction: column;
width: 100%;
}
.error-actions .btn-primary,
.error-actions .btn-ghost {
width: 100%;
}
.error-links-grid {
grid-template-columns: 1fr;
}
}
</style>
{{ end }}

View File

@@ -0,0 +1,7 @@
<div class="md-alert md-alert-note">
<div class="md-mermaid">
<pre class="mermaid">
{{ .Text | safeHTML }}
</pre>
</div>
</div>

View File

@@ -0,0 +1,10 @@
{{- /* Check if this is a GitHub-style alert */ -}}
{{- if eq .Type "alert" -}}
<div class="md-alert md-alert-{{ .AlertType }}">
{{ .Text | safeHTML }}
</div>
{{- else -}}
<blockquote class="md-blockquote">
{{ .Text | safeHTML }}
</blockquote>
{{- end -}}

View File

@@ -0,0 +1,5 @@
<div class="md-mermaid">
<pre class="mermaid">
{{ .Inner | safeHTML }}
</pre>
</div>

View File

@@ -0,0 +1,337 @@
{{- $lang := .Type | default "text" -}}
{{- $filename := .Attributes.filename | default "" -}}
{{- $label := cond (ne $filename "") $filename ($lang | upper) -}}
{{- $id := printf "cb-%s" (printf "%d" .Ordinal | sha256 | truncate 8 "") -}}
{{- $collapseEnabled := site.Params.codeblock.collapse.enabled | default true -}}
{{- $defaultState := site.Params.codeblock.collapse.defaultState | default "expanded" -}}
{{- $collapsedHeight := site.Params.codeblock.collapse.collapsedHeight | default 200 -}}
{{- $highlighted := transform.HighlightCodeBlock . -}}
<div class="mb-codeblock" id="{{ $id }}" data-lang="{{ $lang | lower }}">
<!-- Header with language badge and actions -->
<div class="mb-codeblock-header">
<div class="mb-codeblock-left">
<span class="mb-codeblock-badge">
{{ $lang | upper }}
</span>
{{- if ne $filename "" -}}
<span class="mb-codeblock-filename">
<i class="fas fa-file-code" style="font-size: 0.65rem;"></i>
{{ $filename }}
</span>
{{- end -}}
</div>
<div class="mb-codeblock-actions">
{{- if $collapseEnabled -}}
<button class="mb-action-btn mb-collapse-btn" data-collapsed="false" aria-label="Collapse code">
<i class="fas fa-compress-alt"></i>
<span>Collapse</span>
</button>
{{- end -}}
<button class="mb-action-btn mb-copy-btn" aria-label="Copy code">
<i class="fas fa-copy"></i>
<span>Copy</span>
</button>
</div>
</div>
<!-- Code content -->
<div
class="mb-codeblock-content"
data-state="{{ $defaultState }}"
{{- if eq $defaultState "collapsed" }}
style="max-height: {{ $collapsedHeight }}px; overflow: hidden;"
{{- end }}
>
{{ $highlighted.Wrapped }}
{{- if $collapseEnabled -}}
<div class="mb-collapse-overlay">
<button class="mb-expand-trigger">
<i class="fas fa-chevron-down"></i>
<span>Click to expand</span>
</button>
</div>
{{- end -}}
</div>
</div>
<script>
(function() {
'use strict';
const codeblock = document.getElementById('{{ $id }}');
if (!codeblock) return;
const copyBtn = codeblock.querySelector('.mb-copy-btn');
const collapseBtn = codeblock.querySelector('.mb-collapse-btn');
const content = codeblock.querySelector('.mb-codeblock-content');
const overlay = codeblock.querySelector('.mb-collapse-overlay');
const expandTrigger = overlay?.querySelector('.mb-expand-trigger');
// ==================
// COPY FUNCTIONALITY
// ==================
if (copyBtn) {
copyBtn.addEventListener('click', async function() {
let codeText = '';
// Handle line-numbered code (Hugo's table format)
const codeCell = codeblock.querySelector('.lntd:last-child code');
if (codeCell) {
codeText = codeCell.textContent || codeCell.innerText || '';
} else {
// Regular code block
const codeEl = codeblock.querySelector('pre code');
codeText = codeEl ? (codeEl.textContent || codeEl.innerText || '') : '';
}
try {
await navigator.clipboard.writeText(codeText.trim());
// Success feedback
const originalHTML = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
copyBtn.classList.add('mb-btn-success');
setTimeout(() => {
copyBtn.innerHTML = originalHTML;
copyBtn.classList.remove('mb-btn-success');
}, 2000);
} catch (err) {
console.error('Failed to copy code:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = codeText.trim();
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
copyBtn.innerHTML = '<i class="fas fa-check"></i><span>Copied!</span>';
setTimeout(() => {
copyBtn.innerHTML = '<i class="fas fa-copy"></i><span>Copy</span>';
}, 2000);
} catch (fallbackErr) {
console.error('Fallback copy failed:', fallbackErr);
}
document.body.removeChild(textArea);
}
});
}
// =======================
// COLLAPSE FUNCTIONALITY
// =======================
if (collapseBtn && content && overlay) {
const collapsedHeight = {{ $collapsedHeight }};
function toggleCollapse() {
const isCollapsed = content.dataset.state === 'collapsed';
if (isCollapsed) {
// EXPAND
content.style.maxHeight = '';
content.style.overflow = '';
content.dataset.state = 'expanded';
overlay.style.display = 'none';
collapseBtn.innerHTML = '<i class="fas fa-compress-alt"></i><span>Collapse</span>';
collapseBtn.dataset.collapsed = 'false';
} else {
// COLLAPSE
content.style.maxHeight = collapsedHeight + 'px';
content.style.overflow = 'hidden';
content.dataset.state = 'collapsed';
overlay.style.display = 'flex';
collapseBtn.innerHTML = '<i class="fas fa-expand-alt"></i><span>Expand</span>';
collapseBtn.dataset.collapsed = 'true';
}
}
// Initialize if default state is collapsed
if ('{{ $defaultState }}' === 'collapsed') {
content.dataset.state = 'collapsed';
overlay.style.display = 'flex';
collapseBtn.innerHTML = '<i class="fas fa-expand-alt"></i><span>Expand</span>';
collapseBtn.dataset.collapsed = 'true';
}
collapseBtn.addEventListener('click', toggleCollapse);
if (expandTrigger) {
expandTrigger.addEventListener('click', toggleCollapse);
}
}
})();
</script>
<style>
/* Additional styles for improved codeblock - add to your main.css */
.mb-codeblock-filename {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--color-text);
font-size: 0.75rem;
font-weight: 500;
padding: 0.2rem 0.6rem;
border-radius: 0.35rem;
background: color-mix(in srgb, var(--color-bg) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
}
.mb-action-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
background: transparent;
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
color: var(--color-text-muted);
cursor: pointer;
font-size: 0.7rem;
padding: 0.35rem 0.65rem;
border-radius: 0.4rem;
transition: all 0.15s ease-out;
font-family: inherit;
}
.mb-action-btn:hover {
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
border-color: var(--color-accent);
transform: translateY(-1px);
}
.mb-action-btn:active {
transform: translateY(0);
}
.mb-action-btn i {
font-size: 0.7rem;
}
.mb-btn-success {
color: #22c55e !important;
border-color: #22c55e !important;
background: color-mix(in srgb, #22c55e 12%, transparent) !important;
}
.mb-collapse-overlay {
display: none;
position: absolute;
inset: 0;
background: linear-gradient(
to bottom,
transparent 0%,
rgba(0, 0, 0, 0.3) 40%,
rgba(0, 0, 0, 0.85) 100%
);
align-items: flex-end;
justify-content: center;
padding-bottom: 1rem;
cursor: pointer;
z-index: 10;
}
.mb-expand-trigger {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.4rem 0.8rem;
border-radius: 0.5rem;
border: 1px solid var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 20%, transparent);
color: var(--color-accent);
font-size: 0.75rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease-out;
backdrop-filter: blur(8px);
}
.mb-expand-trigger:hover {
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.3);
}
.mb-expand-trigger i {
font-size: 0.7rem;
animation: bounce 1.5s infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(3px);
}
}
/* Language-specific badge colors */
.mb-codeblock[data-lang="javascript"] .mb-codeblock-badge,
.mb-codeblock[data-lang="js"] .mb-codeblock-badge {
background: color-mix(in srgb, #f7df1e 25%, transparent);
color: #f7df1e;
}
.mb-codeblock[data-lang="typescript"] .mb-codeblock-badge,
.mb-codeblock[data-lang="ts"] .mb-codeblock-badge {
background: color-mix(in srgb, #3178c6 25%, transparent);
color: #3178c6;
}
.mb-codeblock[data-lang="python"] .mb-codeblock-badge,
.mb-codeblock[data-lang="py"] .mb-codeblock-badge {
background: color-mix(in srgb, #3776ab 25%, transparent);
color: #3776ab;
}
.mb-codeblock[data-lang="go"] .mb-codeblock-badge {
background: color-mix(in srgb, #00add8 25%, transparent);
color: #00add8;
}
.mb-codeblock[data-lang="rust"] .mb-codeblock-badge,
.mb-codeblock[data-lang="rs"] .mb-codeblock-badge {
background: color-mix(in srgb, #ce422b 25%, transparent);
color: #ce422b;
}
.mb-codeblock[data-lang="html"] .mb-codeblock-badge {
background: color-mix(in srgb, #e34c26 25%, transparent);
color: #e34c26;
}
.mb-codeblock[data-lang="css"] .mb-codeblock-badge {
background: color-mix(in srgb, #264de4 25%, transparent);
color: #264de4;
}
.mb-codeblock[data-lang="bash"] .mb-codeblock-badge,
.mb-codeblock[data-lang="sh"] .mb-codeblock-badge,
.mb-codeblock[data-lang="shell"] .mb-codeblock-badge {
background: color-mix(in srgb, #4eaa25 25%, transparent);
color: #4eaa25;
}
.mb-codeblock[data-lang="json"] .mb-codeblock-badge {
background: color-mix(in srgb, #000000 25%, transparent);
color: #dddddd;
}
.mb-codeblock[data-lang="yaml"] .mb-codeblock-badge,
.mb-codeblock[data-lang="yml"] .mb-codeblock-badge {
background: color-mix(in srgb, #cb171e 25%, transparent);
color: #cb171e;
}
</style>

View File

@@ -0,0 +1,5 @@
<h{{ .Level }} id="{{ .Anchor }}">
<a href="#{{ .Anchor }}" class="md-heading-anchor">
{{ .Text | safeHTML }}
</a>
</h{{ .Level }}>

View File

@@ -0,0 +1,12 @@
<figure class="md-image">
<a href="{{ .Destination | safeURL }}" class="glightbox">
<img
src="{{ .Destination | safeURL }}"
alt="{{ .Text }}"
loading="lazy"
/>
</a>
{{ with .Title }}
<figcaption>{{ . }}</figcaption>
{{ end }}
</figure>

View File

@@ -0,0 +1,7 @@
<a
href="{{ .Destination | safeURL }}"
{{ if strings.HasPrefix .Destination "http" }}target="_blank" rel="noopener"{{ end }}
class="md-link"
>
{{ .Text | safeHTML }}
</a>

View File

@@ -0,0 +1,39 @@
<div class="table-wrap">
<table
{{- range $k, $v := .Attributes }}
{{- if $v }}
{{- printf " %s=%q" $k $v | safeHTMLAttr }}
{{- end }}
{{- end }}>
<thead>
{{- range .THead }}
<tr>
{{- range . }}
<th
{{- with .Alignment }}
{{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
{{- end -}}
>
{{- .Text -}}
</th>
{{- end }}
</tr>
{{- end }}
</thead>
<tbody>
{{- range .TBody }}
<tr>
{{- range . }}
<td
{{- with .Alignment }}
{{- printf " style=%q" (printf "text-align: %s" .) | safeHTMLAttr }}
{{- end -}}
>
{{- .Text -}}
</td>
{{- end }}
</tr>
{{- end }}
</tbody>
</table>
</div>

View File

@@ -0,0 +1,4 @@
{{- $type := .Get "type" | default "note" -}}
<div class="md-alert md-alert-{{ $type }}">
{{ .Inner | markdownify }}
</div>

View File

@@ -0,0 +1,35 @@
{{- $images := split (.Get "images") "," -}}
{{- $captions := split (default "" (.Get "captions")) "," -}}
<div class="gallery-container" data-jg>
{{- if .Inner -}}
{{- /* If Inner content is provided, convert markdown images to lightbox-ready anchors */ -}}
{{- $content := .Inner -}}
{{- $content = replaceRE `!\[([^\]]*)\]\(([^\)]+)\)` `<a href="$2" class="glightbox" data-glightbox="description: $1"><img src="$2" alt="$1" loading="lazy"></a>` $content -}}
{{ $content | safeHTML }}
{{- else if $images -}}
{{- /* Otherwise, generate from images parameter */ -}}
{{- range $index, $image := $images -}}
{{- $imagePath := trim $image " " -}}
{{- $caption := "" -}}
{{- if $captions -}}
{{- $caption = index $captions $index | default "" | trim " " -}}
{{- end -}}
{{- if $imagePath -}}
<a href="{{ $imagePath | relURL }}" class="glightbox" data-glightbox="{{ if $caption }}description: {{ $caption }}{{ end }}">
<img src="{{ $imagePath | relURL }}" alt="{{ $caption | default $imagePath }}" loading="lazy">
</a>
{{- end -}}
{{- end -}}
{{- else -}}
{{- /* Fallback: try to get images from page resources */ -}}
{{- $resources := .Page.Resources.Match "images/*" -}}
{{- if $resources -}}
{{- range $resources -}}
<a href="{{ .RelPermalink }}" class="glightbox">
<img src="{{ .Resize "300x" }}" alt="{{ .Name }}" loading="lazy">
</a>
{{- end -}}
{{- end -}}
{{- end -}}
</div>

View File

@@ -0,0 +1,156 @@
{{ define "main" }}
<section class="layout-page">
<article class="about-alt-page">
<div class="about-alt-layout">
<!-- Left Sidebar - Profile Card -->
<aside class="about-alt-sidebar">
<div class="about-alt-profile-card">
{{ with .Site.Params.hero.avatar }}
<div class="about-alt-avatar">
<img src="{{ . | relURL }}" alt="{{ $.Site.Params.brand }}" />
</div>
{{ else }}
<div class="about-alt-avatar-placeholder">
<i class="fas fa-code"></i>
</div>
{{ end }}
<h1 class="about-alt-name">{{ $.Site.Params.brand }}</h1>
{{ with .Params.subtitle }}
<p class="about-alt-role">{{ . }}</p>
{{ end }}
{{ with .Site.Params.hero.location }}
<div class="about-alt-meta">
<i class="fas fa-map-marker-alt"></i>
<span>{{ . }}</span>
</div>
{{ end }}
<!-- Quick Stats -->
{{ with $.Site.Params.about.alt.stats }}
<div class="about-alt-stats">
{{ range . }}
<div class="about-alt-stat">
<div class="about-alt-stat-value">{{ .value }}</div>
<div class="about-alt-stat-label">{{ .label }}</div>
</div>
{{ end }}
</div>
{{ end }}
<!-- Social Links -->
{{ with .Site.Params.social }}
<div class="about-alt-social">
{{ range . }}
<a
href="{{ .url }}"
class="about-alt-social-icon"
target="_blank"
rel="noopener noreferrer"
aria-label="{{ .label }}"
>
<i class="{{ .icon }}"></i>
</a>
{{ end }}
</div>
{{ end }}
</div>
</aside>
<!-- Right Content Area -->
<div class="about-alt-content">
<!-- Introduction -->
<div class="about-alt-section">
<div class="markdown-body">
{{ $content := .Content }}
{{ $content = replace $content "<hr>" "|||SPLIT|||" }}
{{ $content = replace $content "<hr />" "|||SPLIT|||" }}
{{ $content = replace $content "<hr/>" "|||SPLIT|||" }}
{{ $parts := split $content "|||SPLIT|||" }}
{{ index $parts 0 | safeHTML }}
</div>
</div>
<!-- Experience Cards -->
{{ if gt (len $parts) 1 }}
{{- $experienceParts := slice -}}
{{- $additionalContent := "" -}}
{{- $foundNonExperience := false -}}
{{- range $index, $part := after 1 $parts -}}
{{- $trimmed := trim $part " \n\t" -}}
{{- if $trimmed -}}
{{- /* Check if this looks like an experience card (starts with <p><strong>) or is additional content (starts with <h) */ -}}
{{- if or (hasPrefix $trimmed "<p><strong>") (hasPrefix $trimmed "<p><strong ") (hasPrefix $trimmed "<p>\n<strong") (hasPrefix $trimmed "<p> <strong") -}}
{{- if not $foundNonExperience -}}
{{- $experienceParts = $experienceParts | append $part -}}
{{- else -}}
{{- $additionalContent = printf "%s|||SPLIT|||%s" $additionalContent $part -}}
{{- end -}}
{{- else -}}
{{- $foundNonExperience = true -}}
{{- $additionalContent = printf "%s|||SPLIT|||%s" $additionalContent $part -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if gt (len $experienceParts) 0 -}}
<div class="about-alt-section">
<h2 class="about-alt-section-title">
<i class="fas fa-briefcase"></i>
<span>Experience</span>
</h2>
<div class="about-alt-experience-grid">
{{- range $experienceParts -}}
<div class="about-alt-experience-card">
{{ . | safeHTML }}
</div>
{{- end -}}
</div>
</div>
{{- end -}}
{{- /* Render additional content sections */ -}}
{{- if $additionalContent -}}
{{- $additionalParts := split $additionalContent "|||SPLIT|||" -}}
{{- range $additionalParts -}}
{{- $trimmed := trim . " \n\t" -}}
{{- if $trimmed -}}
<div class="about-alt-section">
<div class="markdown-body">
{{ . | safeHTML }}
</div>
</div>
{{- end -}}
{{- end -}}
{{- end -}}
{{ end }}
<!-- Skills Badge Cloud -->
{{ with $.Site.Params.about.alt.skills }}
<div class="about-alt-section">
<h2 class="about-alt-section-title">
<i class="fas fa-code"></i>
<span>Tech Stack</span>
</h2>
<div class="about-alt-skills">
{{ range . }}
<span class="about-alt-skill">
{{ with .icon }}<i class="{{ . }}"></i>{{ end }}
{{ .label }}
</span>
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
</article>
</section>
{{ end }}

View File

@@ -0,0 +1,77 @@
{{ define "main" }}
<section class="layout-page">
<article class="about-page">
<!-- Hero Section -->
<header class="about-hero">
<div class="about-hero-content">
{{ with .Site.Params.hero.avatar }}
<div class="about-avatar">
<img src="{{ . | relURL }}" alt="{{ $.Site.Params.brand }}" />
</div>
{{ end }}
<h1 class="about-title">{{ .Title }}</h1>
{{ with .Params.subtitle }}
<p class="about-subtitle">{{ . }}</p>
{{ end }}
</div>
</header>
<!-- Main Content -->
<div class="about-content">
<div class="card card-pad markdown-body">
{{ $content := .Content }}
{{ $content = replace $content "<hr>" "|||SPLIT|||" }}
{{ $content = replace $content "<hr />" "|||SPLIT|||" }}
{{ $content = replace $content "<hr/>" "|||SPLIT|||" }}
{{ $parts := split $content "|||SPLIT|||" }}
{{ if eq (len $parts) 1 }}
<!-- No timeline, render normally -->
{{ .Content }}
{{ else }}
<!-- Render intro section (everything before first hr) -->
{{ index $parts 0 | safeHTML }}
<!-- Render timeline -->
<div class="timeline">
{{ range after 1 $parts }}
{{ $trimmed := trim . " \n\t" }}
{{ if $trimmed }}
<div class="timeline-item">
<div class="timeline-marker"></div>
<div class="timeline-content">
{{ . | safeHTML }}
</div>
</div>
{{ end }}
{{ end }}
</div>
{{ end }}
</div>
</div>
<!-- Social Links Footer -->
{{ with .Site.Params.social }}
<div class="about-social">
<h3 class="about-social-title">Let's Connect</h3>
<div class="about-social-links">
{{ range . }}
<a
href="{{ .url }}"
class="about-social-link"
target="_blank"
rel="noopener noreferrer"
aria-label="{{ .label }}"
>
<i class="{{ .icon }}"></i>
<span>{{ .label }}</span>
</a>
{{ end }}
</div>
</div>
{{ end }}
</article>
</section>
{{ end }}

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ .Site.Language.Lang }}"
data-cb-collapse-enabled="{{ .Site.Params.features.codeblock.collapse.enabled }}"
data-cb-collapse-default="{{ .Site.Params.features.codeblock.collapse.defaultState }}"
data-cb-collapse-lines="{{ .Site.Params.features.codeblock.collapse.autoCollapseLines }}"
data-cb-collapse-auto-height="{{ .Site.Params.features.codeblock.collapse.autoCollapseHeight }}"
data-cb-collapse-collapsed-height="{{ .Site.Params.features.codeblock.collapse.collapsedHeight }}">
<head>
{{ partial "head.html" . }}
</head>
<body class="min-h-screen bg-bg text-text antialiased">
<div class="flex min-h-screen flex-col">
{{ partial "header.html" . }}
<main class="flex-1">
{{ block "main" . }}{{ end }}
</main>
{{ partial "footer.html" . }}
</div>
{{ partial "search-overlay.html" . }}
{{ partial "dock.html" . }}
<script src="{{ "js/main.js" | relURL }}" defer></script>
<script src="{{ "js/search.js" | relURL }}" defer></script>
<script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/justified-gallery@3.8.2/dist/js/justifiedGallery.min.js" defer></script>
<script src="{{ "js/lightbox.js" | relURL }}" defer></script>
<script src="{{ "js/gallery.js" | relURL }}" defer></script>
</body>
</html>

View File

@@ -0,0 +1,30 @@
{{ define "main" }}
<section class="layout-page-tight">
<div class="page-int">
<header class="mb-8">
<h1 class="heading-page text-2xl sm:text-3xl">{{ .Title }}</h1>
{{ with .Description }}
<p class="mt-2 text-sm text-muted">{{ . }}</p>
{{ end }}
</header>
<div class="space-y-6">
{{ range .Pages.ByDate.Reverse }}
<article class="border-b border-border pb-6 last:border-0">
<a href="{{ .RelPermalink }}" class="group block">
<h2 class="text-lg font-medium tracking-tight group-hover:text-accent">
{{ .Title }}
</h2>
{{ with .Params.description }}
<p class="mt-1 text-sm text-muted">{{ . }}</p>
{{ end }} {{ with .Date }}
<p class="mt-2 text-xs text-muted">{{ .Format "January 2, 2006" }}</p>
{{ end }}
</a>
</article>
{{ end }}
</div>
</section>
{{ end }}
</div>

View File

@@ -0,0 +1,154 @@
{{ define "main" }}
<section class="layout-page section-stack">
<div class="article-layout">
<article class="article-main space-y-4">
<header class="space-y-2">
<h1 class="heading-page text-2xl sm:text-3xl">{{ .Title }}</h1>
<div class="text-[0.75rem] text-muted">
{{ with .Date }}
<span>{{ .Format "02 Jan 2006" }}</span>
{{ end }}
{{ with .ReadingTime }}
<span> • {{ . }} min read</span>
{{ end }}
</div>
{{ with .Params.description }}
<p class="max-w-xl text-sm text-muted">
{{ . }}
</p>
{{ end }}
</header>
<div class="card card-pad">
<div class="markdown-body">
{{ .Content }}
</div>
</div>
</article>
{{ if .TableOfContents }}
<aside class="article-toc">
<div class="toc-wrapper">
<div class="toc-header">
<h3 class="toc-title">
<i class="fas fa-list-ul"></i>
<span>Table of Contents</span>
</h3>
<button class="toc-toggle" aria-label="Toggle table of contents">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<nav class="toc-nav">
{{ .TableOfContents }}
</nav>
</div>
</aside>
<script>
(function() {
'use strict';
const tocWrapper = document.querySelector('.toc-wrapper');
const tocToggle = document.querySelector('.toc-toggle');
const tocNav = document.querySelector('.toc-nav');
const tocLinks = document.querySelectorAll('.toc-nav a');
if (!tocWrapper || !tocNav) return;
// =====================
// MOBILE TOGGLE
// =====================
if (tocToggle) {
tocToggle.addEventListener('click', function() {
tocWrapper.classList.toggle('collapsed');
});
}
// =====================
// SMOOTH SCROLLING
// =====================
tocLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
const yOffset = -80; // offset for sticky header
const y = targetElement.getBoundingClientRect().top + window.pageYOffset + yOffset;
window.scrollTo({
top: y,
behavior: 'smooth'
});
// Update URL without scrolling
history.pushState(null, null, targetId);
}
});
});
// =====================
// ACTIVE LINK HIGHLIGHT
// =====================
const headings = document.querySelectorAll('.markdown-body h1[id], .markdown-body h2[id], .markdown-body h3[id], .markdown-body h4[id], .markdown-body h5[id], .markdown-body h6[id]');
let activeHeading = null;
const observer = new IntersectionObserver(
(entries) => {
// Find all currently intersecting headings
const intersectingHeadings = [];
entries.forEach((entry) => {
if (entry.isIntersecting) {
intersectingHeadings.push({
element: entry.target,
top: entry.boundingClientRect.top
});
}
});
// If we have intersecting headings, find the one closest to the top
if (intersectingHeadings.length > 0) {
// Sort by distance from top (smallest positive value or largest negative)
intersectingHeadings.sort((a, b) => Math.abs(a.top) - Math.abs(b.top));
const closestHeading = intersectingHeadings[0].element;
const id = closestHeading.getAttribute('id');
if (id && id !== activeHeading) {
activeHeading = id;
const tocLink = document.querySelector(`.toc-nav a[href="#${id}"]`);
if (tocLink) {
// Remove active from all links
tocLinks.forEach(link => link.classList.remove('active'));
// Add active to current link
tocLink.classList.add('active');
}
}
}
},
{
rootMargin: '-80px 0px -80%',
threshold: [0, 0.25, 0.5, 0.75, 1]
}
);
// Observe all headings
headings.forEach((heading) => {
observer.observe(heading);
});
// Highlight first item by default
if (tocLinks.length > 0) {
tocLinks[0].classList.add('active');
activeHeading = tocLinks[0].getAttribute('href').substring(1);
}
})();
</script>
{{ end }}
</div>
</section>
{{ end }}

View File

@@ -0,0 +1,48 @@
{{ define "main" }}
<section class="layout-page">
<div class="page-int section-stack">
<header class="space-y-2">
<h1 class="heading-page text-2xl sm:text-3xl">Blog</h1>
{{ with .Site.Params.blogIntro }}
<p class="max-w-xl text-sm text-muted">
{{ . }}
</p>
{{ else }}
<p class="max-w-xl text-sm text-muted">
Notes on engineering, design, and building small, thoughtful tools.
</p>
{{ end }}
</header>
{{/* Paginate all posts in this section */}}
{{ $paginator := .Paginate .Pages }}
<div class="grid gap-4 md:grid-cols-2">
{{ range $paginator.Pages.ByDate.Reverse }}
{{ partial "components/post-card.html" (dict "Page" . "Root" $) }}
{{ end }}
</div>
{{/* Simple pagination controls */}}
{{ if gt $paginator.TotalPages 1 }}
<nav class="mt-6 flex items-center justify-between text-xs text-muted">
{{ if $paginator.HasPrev }}
<a href="{{ $paginator.Prev.URL }}" class="link-underline">
← Newer posts
</a>
{{ else }}
<span></span>
{{ end }}
{{ if $paginator.HasNext }}
<a href="{{ $paginator.Next.URL }}" class="link-underline">
Older posts →
</a>
{{ else }}
<span></span>
{{ end }}
</nav>
{{ end }}
</div>
</section>
{{ end }}

View File

@@ -0,0 +1,12 @@
{{ define "main" }}
<section class="layout-page">
<div class="page-int section-stack section-stack--home">
{{ $default := slice "hero" "now" "tech-marquee" "projects" "posts" }}
{{ $sections := .Site.Params.home.sections | default $default }}
{{ range $sections }}
{{ partial (printf "home/%s.html" .) $ }}
{{ end }}
</div>
</section>
{{ end }}

View File

@@ -0,0 +1,17 @@
{{- $pages := slice -}}
{{- range .Site.RegularPages -}}
{{- $summary := .Summary -}}
{{- if not $summary -}}
{{- $summary = .Description -}}
{{- end -}}
{{- $pages = $pages | append (dict
"title" .Title
"permalink" .Permalink
"section" .Section
"summary" (plainify $summary)
) -}}
{{- end -}}
{{- dict "pages" $pages | jsonify -}}

View File

@@ -0,0 +1,61 @@
{{- $title := .Site.Title -}}
{{- $shortName := .Site.Params.brand | default .Site.Title -}}
{{- $description := .Site.Params.description | default "A minimal, dark-mode first personal site" -}}
{{- $themeColor := .Site.Params.manifest.themeColor | default "#a855f7" -}}
{{- $bgColor := .Site.Params.manifest.backgroundColor | default "#000000" -}}
{{- $lang := .Site.Language.Lang | default "en" -}}
{
"name": "{{ $title }}",
"short_name": "{{ $shortName }}",
"description": "{{ $description }}",
"start_url": "{{ "/" | relURL }}",
"scope": "{{ "/" | relURL }}",
"display": "standalone",
"background_color": "{{ $bgColor }}",
"theme_color": "{{ $themeColor }}",
"orientation": "portrait-primary",
"icons": [
{{- if .Site.Params.manifest.icons }}
{{- range $i, $icon := .Site.Params.manifest.icons }}
{{- if $i }},{{ end }}
{
"src": "{{ $icon.src | relURL }}",
"sizes": "{{ $icon.sizes }}",
"type": "{{ $icon.type | default "image/png" }}"
{{- with $icon.purpose }},"purpose": "{{ . }}"{{ end }}
}
{{- end }}
{{- else }}
{
"src": "/icons/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/apple-touch-icon.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "/icons/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "/icons/favicon-16x16.png",
"sizes": "16x16",
"type": "image/png"
}
{{- end }}
],
"categories": "{{ .Site.Params.manifest.categories | default (slice "blog" "portfolio" "developer") }}",
"lang": "{{ $lang }}",
"dir": "ltr"
}

View File

@@ -0,0 +1,46 @@
{{- if not hugo.IsServer -}}
<!-- Google Analytics (GA4) -->
{{ with .Site.Params.analytics.googleAnalytics }}
{{ if . }}
<script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '{{ . }}');
</script>
{{ end }}
{{ end }}
<!-- Plausible Analytics -->
{{ with .Site.Params.analytics.plausible }}
{{ if .enabled }}
<script defer data-domain="{{ .domain }}" src="{{ default "https://plausible.io/js/script.js" .scriptUrl }}"></script>
{{ end }}
{{ end }}
<!-- Umami Analytics -->
{{ with .Site.Params.analytics.umami }}
{{ if .enabled }}
<script defer src="{{ .scriptUrl }}" data-website-id="{{ .websiteId }}"></script>
{{ end }}
{{ end }}
<!-- Fathom Analytics -->
{{ with .Site.Params.analytics.fathom }}
{{ if .enabled }}
<script src="{{ .scriptUrl }}" data-site="{{ .siteId }}" defer></script>
{{ end }}
{{ end }}
<!-- Custom Analytics Scripts -->
{{ with .Site.Params.analytics.custom }}
{{ if .head }}
{{ range .head }}
{{ . | safeHTML }}
{{ end }}
{{ end }}
{{ end }}
{{- end -}}

View File

@@ -0,0 +1,53 @@
{{/* props: Page (post page) */}}
{{- $p := .Page -}}
{{- $icon := $p.Params.icon | default "fa-regular fa-file-lines" -}}
{{- $category := $p.Params.category | default "Article" -}}
<article class="card card-pad card-home card-home--post group">
<a href="{{ $p.RelPermalink }}" class="card-home-body">
<div class="card-home-header">
<div class="card-home-icon card-home-icon--post">
<i class="{{ $icon }}"></i>
</div>
<div class="min-w-0">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<h3 class="truncate text-sm font-semibold tracking-tight group-hover:text-accent">
{{ $p.Title }}
</h3>
<div class="mt-0.5 flex flex-wrap items-center gap-2 text-[0.68rem] text-muted">
{{ with $p.Date }}
<span>{{ .Format "02 Jan 2006" }}</span>
{{ end }}
{{ with $p.ReadingTime }}
<span>• {{ . }} min read</span>
{{ end }}
</div>
</div>
{{ with $category }}
<span class="card-badge card-badge--soft">
{{ . }}
</span>
{{ end }}
</div>
</div>
</div>
{{ with $p.Params.description }}
<p class="mt-2 text-xs text-muted">
{{ . }}
</p>
{{ end }}
{{ with $p.Params.tags }}
<div class="mt-3 card-tag-row">
{{ range first 3 . }}
<span class="card-tag-pill">{{ . }}</span>
{{ end }}
</div>
{{ end }}
</a>
</article>

View File

@@ -0,0 +1,87 @@
{{/* props: Page (project page) */}}
{{- $p := .Page -}}
{{- $icon := $p.Params.icon | default "fa-solid fa-folder-tree" -}}
{{- $badge := cond ($p.Params.featured) "Featured" "" -}}
{{- $repo := $p.Params.repo -}}
{{- $repoIcon := $p.Params.repoIcon | default "fa-brands fa-github" -}}
{{- $repoLabel := $p.Params.repoLabel | default "Repo" -}}
{{- $demo := $p.Params.demo -}}
{{- $demoIcon := $p.Params.demoIcon | default "fa-solid fa-play" -}}
{{- $demoLabel := $p.Params.demoLabel | default "Demo" -}}
{{- $website := $p.Params.website -}}
{{- $websiteIcon := $p.Params.websiteIcon | default "fa-solid fa-globe" -}}
{{- $websiteLabel := $p.Params.websiteLabel | default "Website" -}}
<article class="card card-pad card-home card-home--project group">
<!-- Entire main body is clickable -->
<a href="{{ $p.RelPermalink }}" class="card-home-body">
<div class="card-home-header">
<div class="card-home-icon">
<i class="{{ $icon }}"></i>
</div>
<div class="min-w-0">
<div class="inline-flex items-center gap-1">
<h3 class="truncate text-sm font-semibold tracking-tight group-hover:text-accent">
{{ $p.Title }}
</h3>
{{ with $badge }}
<span class="card-badge">{{ . }}</span>
{{ end }}
</div>
{{ with $p.Params.subtitle }}
<p class="mt-0.5 truncate text-[0.7rem] text-muted">
{{ . }}
</p>
{{ end }}
</div>
</div>
{{ with $p.Params.description }}
<p class="mt-2 text-xs text-muted">
{{ . }}
</p>
{{ end }}
{{ with $p.Params.stack }}
<div class="mt-3 card-tag-row">
{{ range . }}
<span class="card-tag-pill">{{ . }}</span>
{{ end }}
</div>
{{ end }}
</a>
<!-- Footer buttons: repo, demo, website -->
<div class="card-home-footer card-home-footer--buttons">
<div class="flex items-center gap-2">
{{ with $repo }}
<a href="{{ . }}" class="card-cta-btn" target="_blank" rel="noopener noreferrer">
<i class="{{ $repoIcon }} text-[0.75rem]"></i>
<span>{{ $repoLabel }}</span>
</a>
{{ end }}
{{ with $demo }}
<a href="{{ . }}" class="card-cta-btn" target="_blank" rel="noopener noreferrer">
<i class="{{ $demoIcon }} text-[0.75rem]"></i>
<span>{{ $demoLabel }}</span>
</a>
{{ end }}
{{ with $website }}
<a href="{{ . }}" class="card-cta-btn" target="_blank" rel="noopener noreferrer">
<i class="{{ $websiteIcon }} text-[0.75rem]"></i>
<span>{{ $websiteLabel }}</span>
</a>
{{ end }}
</div>
</div>
</article>

View File

@@ -0,0 +1,12 @@
{{/* props: Title, Url, Small (bool) */}} {{- $title := .Title | default
"Section" -}} {{- $url := .Url -}} {{- $small := .Small | default false -}}
<div class="flex items-baseline justify-between gap-2">
<h2 class="heading-section">{{ $title }}</h2>
{{ with $url }}
<a href="{{ . | relURL }}" class="link-underline text-[0.72rem] text-muted">
{{ if $small }}All{{ else }}View all{{ end }}
</a>
{{ end }}
</div>

View File

@@ -0,0 +1,41 @@
<button
type="button"
class="theme-toggle"
aria-label="Toggle dark mode"
data-theme-toggle
>
<span data-theme-icon-light style="display: none">
<!-- Sun icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="4" />
<path
d="M12 3v2m0 14v2m9-9h-2M5 12H3m15.364-6.364-1.414 1.414M8.05 15.95l-1.414 1.414m0-11.314L8.05 8.05m9.9 9.9-1.414-1.414"
/>
</svg>
</span>
<span data-theme-icon-dark>
<!-- Moon icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12.79A9 9 0 0 1 12.21 3 7 7 0 1 0 21 12.79z" />
</svg>
</span>
</button>

View File

@@ -0,0 +1,55 @@
{{- $isHome := .IsHome -}}
<div class="dock" data-dock>
<div class="dock-inner">
<!-- Actions panel -->
<div class="dock-panel" data-dock-panel>
{{ if not $isHome }}
<!-- Back -->
<button
type="button"
class="dock-action"
data-dock-action="back"
aria-label="Go back"
>
<i class="fa-solid fa-arrow-left text-[0.7rem]"></i>
</button>
<span class="dock-divider" aria-hidden="true"></span>
{{ end }}
<!-- Search -->
<button
type="button"
class="dock-action"
data-dock-action="search"
aria-label="Search"
onclick="window.MinimalSearch && window.MinimalSearch.open()"
>
<i class="fa-solid fa-magnifying-glass text-[0.7rem]"></i>
</button>
<!-- Divider -->
<span class="dock-divider" aria-hidden="true"></span>
<!-- Back to top -->
<button
type="button"
class="dock-action"
data-dock-action="top"
aria-label="Back to top"
>
<i class="fa-solid fa-arrow-up text-[0.7rem]"></i>
</button>
</div>
<!-- Toggle -->
<button
type="button"
class="dock-toggle"
aria-label="Open quick actions"
data-dock-toggle
>
<span class="dock-toggle-dots"></span>
</button>
</div>
</div>

View File

@@ -0,0 +1,56 @@
<footer class="pb-6 pt-10">
<div class="mx-auto max-w-7xl px-4 sm:px-6">
<div class="footer-shell">
<div class="footer-inner">
<!-- Left -->
<div class="space-y-1">
<p class="footer-small">
&copy; {{ now.Format "2006" }} {{ .Site.Title }} — All rights
reserved.
</p>
<p class="footer-small flex flex-wrap items-center gap-1">
<span>Built with</span>
<a
href="https://gohugo.io/"
class="footer-link footer-float link-underline"
target="_blank"
rel="noopener noreferrer"
>
<span>Hugo</span>
</a>
<span>/</span>
<a
href="{{ .Site.Params.themeGit }}"
class="footer-link footer-float link-underline"
target="_blank"
rel="noopener noreferrer"
>
<span>Minimal&nbsp;Black</span>
</a>
</p>
</div>
<!-- Right -->
{{ with .Site.Params.social }}
<div class="footer-links">
{{ range . }}
<a
href="{{ .url }}"
class="footer-link footer-float link-underline"
target="_blank"
rel="noopener noreferrer"
>
<i class="{{ .icon }} text-[0.8rem]"></i>
<span>{{ .label }}</span>
</a>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
</footer>

View File

@@ -0,0 +1,103 @@
{{- $title := cond (ne .Title "") (printf "%s | %s" .Title .Site.Title)
.Site.Title -}}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{{ $title }}</title>
{{ partial "meta.html" . }}
<!-- Favicon -->
{{ with .Site.Params.favicon }}
<link rel="icon" type="image/x-icon" href="{{ . | relURL }}" />
{{ else }}
<!-- Default favicon paths -->
{{ if fileExists "static/favicon.ico" }}
<link rel="icon" type="image/x-icon" href="{{ "favicon.ico" | relURL }}" />
{{ end }}
{{ if fileExists "static/favicon.png" }}
<link rel="icon" type="image/png" href="{{ "favicon.png" | relURL }}" />
{{ end }}
{{ if fileExists "static/favicon.svg" }}
<link rel="icon" type="image/svg+xml" href="{{ "favicon.svg" | relURL }}" />
{{ end }}
{{ end }}
<!-- Apple Touch Icon -->
{{ if .Site.Params.appleTouchIcon }}
<link rel="apple-touch-icon" href="{{ . | relURL }}" />
{{ else if fileExists "static/apple-touch-icon.png" }}
<link rel="apple-touch-icon" href="{{ "apple-touch-icon.png" | relURL }}" />
{{ end }}
<!-- Web App Manifest -->
{{ with .Site.GetPage "/" }}
{{ range .OutputFormats }}
{{ if eq .Name "webappmanifest" }}
<link rel="manifest" href="{{ .Permalink }}" />
{{ end }}
{{ end }}
{{ end }}
<link rel="stylesheet" href="{{ "css/main.css" | relURL }}">
{{ if .Site.Params.icons.useFontAwesome }}
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"
>
{{ end }}
{{ if .Site.Params.icons.useDevicon }}
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/devicons/devicon@v2.17.0/devicon.min.css"
>
{{ end }}
<!-- GLightbox -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css"
/>
<!-- Justified Gallery (Vanilla) -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/justified-gallery@3.8.2/dist/css/justifiedGallery.min.css"
/>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({ theme: "dark" });
</script>
<script>
(function () {
try {
var stored = localStorage.getItem("theme");
var systemDark =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches;
var defaultTheme =
'{{ default "system" .Site.Params.theme.defaultTheme }}';
var theme =
stored ||
(defaultTheme === "dark"
? "dark"
: defaultTheme === "light"
? "light"
: systemDark
? "dark"
: "light");
document.documentElement.setAttribute("data-theme", theme);
} catch (e) {
document.documentElement.setAttribute("data-theme", "light");
}
})();
</script>
<!-- Analytics -->
{{ partial "analytics.html" . }}

View File

@@ -0,0 +1,86 @@
<header class="pt-6">
{{/* Brand string for logo/monogram */}}
{{- $brand := .Site.Params.brand | default .Site.Title}}
{{- $mono := upper (substr $brand 0 2) -}}
<div class="mx-auto max-w-7xl px-4 sm:px-6">
<!-- Floating nav container -->
<div class="nav-shell">
<div class="nav-inner">
<!-- Brand: logo or monogram, always links to root -->
<a href="{{ "/" | relURL }}" class="flex items-center gap-2">
{{ with .Site.Params.logo }}
<div class="flex h-8 w-8 items-center justify-center overflow-hidden rounded-full bg-surface">
<img
src="{{ . | relURL }}"
alt="{{ $brand }}"
class="h-full w-full object-cover"
>
</div>
{{ else }}
<div class="logo-badge">
{{ $mono }}
</div>
{{ end }}
<span class="text-xs font-semibold tracking-wide">
{{ $brand }}
</span>
</a>
<div class="flex items-center gap-2">
<!-- Desktop nav (configurable) -->
<nav class="hidden items-center gap-5 md:flex">
{{- $current := . -}}
{{- range .Site.Menus.main }}
<a
href="{{ .URL | relURL }}"
class="nav-link link-underline flex items-center gap-1 {{ if $current.IsMenuCurrent "main" . }}text-text{{ end }}"
>
{{ with .Params.icon }}
<i class="{{ . }} text-[0.75rem]"></i>
{{ end }}
<span>{{ .Name }}</span>
</a>
{{- end }}
</nav>
<!-- Theme toggle, sized to match nav -->
{{ partial "dark-toggle.html" . }}
<!-- Mobile menu button -->
<button
type="button"
class="ml-1 inline-flex items-center justify-center rounded-full border border-border bg-bg p-2 text-muted shadow-sm hover:text-accent md:hidden"
aria-label="Toggle navigation"
data-mobile-nav-toggle
>
<span class="sr-only">Open navigation</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="6" x2="20" y2="6" />
<line x1="4" y1="12" x2="20" y2="12" />
<line x1="4" y1="18" x2="20" y2="18" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Mobile nav (uses same menu config) -->
<div class="mx-auto mt-2 max-w-7xl px-4 sm:px-6 md:hidden">
<nav
class="hidden rounded-2xl border border-border bg-surface px-4 py-3 text-sm text-muted shadow-md"
data-mobile-nav
>
{{ range .Site.Menus.main }}
<a href="{{ .URL | relURL }}" class="flex items-center gap-2 py-1.5">
{{ with .Params.icon }}
<i class="{{ . }} text-[0.8rem]"></i>
{{ end }}
<span>{{ .Name }}</span>
</a>
{{ end }}
</nav>
</div>
</header>

View File

@@ -0,0 +1,94 @@
{{- $hero := .Site.Params.hero -}}
<div class="grid gap-8 md:grid-cols-[minmax(0,2fr)_minmax(0,1.2fr)] items-start">
<div class="space-y-5 animate-fade-up">
<!-- Badge + availability -->
<div class="flex flex-wrap items-center gap-4">
{{ with $hero.badge }}
<p class="eyebrow font-medium text-sm text-accent">{{ . }}</p>
{{ end }}
{{ if $hero.available }}
<span class="badge-available px-3">
<span class="h-1.5 w-1.5 rounded-full bg-white/95"></span>
<span>{{ default "Available for work" $hero.availableLabel }}</span>
</span>
{{ end }}
</div>
<div class="space-y-2">
<h1 class="heading-page text-3xl sm:text-4xl">
{{ default "Hi, Im Your Name." $hero.title }}
</h1>
{{ with $hero.role }}
<p class="text-sm font-medium text-muted">
{{ . }}
</p>
{{ end }}
</div>
{{ with $hero.summary }}
<p class="max-w-xl text-sm text-muted">
{{ . }}
</p>
{{ end }}
<!-- Meta row: location + focus -->
<div class="mt-3 flex flex-wrap gap-2 text-[0.7rem] text-muted">
{{ with $hero.location }}
<span class="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/90 px-3 py-1.5">
<i class="fa-solid fa-location-dot text-[0.8rem]"></i>
<span class="font-medium">{{ . }}</span>
</span>
{{ end }}
{{ with $hero.focus }}
<span class="inline-flex items-center gap-1.5 rounded-full border border-border bg-surface/80 px-3 py-1.5">
<i class="fa-regular fa-circle-dot text-[0.8rem]"></i>
<span>{{ . }}</span>
</span>
{{ end }}
</div>
<!-- Optional highlight pills -->
{{ with $hero.highlights }}
<div class="mt-3 flex flex-wrap gap-2">
{{ range . }}
<span class="inline-flex items-center rounded-full border border-border bg-surface/80 px-3 py-1 text-[0.7rem] text-muted">
{{ .label }}
</span>
{{ end }}
</div>
{{ end }}
<!-- CTAs -->
<div class="mt-4 flex flex-wrap items-center gap-3">
{{ with $hero.primary }}
<a href="{{ .href | default "/projects/" }}" class="btn-primary">
{{ .label | default "View projects" }}
</a>
{{ end }}
{{ with $hero.secondary }}
<a href="{{ .href | default "/blog/" }}" class="btn-ghost link-underline">
{{ .label | default "Read the blog" }}
</a>
{{ end }}
</div>
</div>
<!-- Avatar ONLY if provided -->
{{ with $hero.avatar }}
<div class="order-first md:order-none">
<div class="card card-pad flex items-center justify-center md:justify-end">
<div class="h-24 w-24 sm:h-28 sm:w-28 overflow-hidden rounded-2xl border border-border bg-surface shadow-md">
<img
src="{{ . | relURL }}"
alt="{{ $hero.title | default $.Site.Title }}"
class="h-full w-full object-cover"
/>
</div>
</div>
</div>
{{ end }}
</div>

View File

@@ -0,0 +1,28 @@
{{- $hero := .Site.Params.hero -}}
{{- $home := .Site.Params.home -}}
{{ if $home.showNowSection }}
<div class="space-y-4">
<div class="space-y-3">
<h2 class="heading-section">{{ default "Now" $hero.nowLabel }}</h2>
<div class="card card-pad text-xs text-muted">
{{ with $hero.nowIntro }}
<p class="mb-2">{{ . }}</p>
{{ end }}
{{ with $hero.now }}
<ul class="list-disc space-y-1 pl-4">
{{ range . }}
<li>{{ . }}</li>
{{ end }}
</ul>
{{ else }}
<p>
Add a <code>hero.now</code> list in your config to describe what youre
focused on right now.
</p>
{{ end }}
</div>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,38 @@
{{- $home := .Site.Params.home -}}
{{ if ne $home.showLatestPosts false }}
<div class="space-y-3 animate-fade-up">
<div class="space-y-1">
<h2 class="heading-section">
{{ default "Latest writing" $home.blogTitle }}
</h2>
{{ with $home.blogSubtitle }}
<p class="text-xs text-muted">
{{ . }}
</p>
{{ end }}
</div>
{{ $limit := cond (gt (int $home.latestPostsLimit) 0) (int $home.latestPostsLimit) 3 }}
{{ $posts := first $limit (where .Site.RegularPages "Section" "blog") }}
<div class="grid gap-4 md:grid-cols-2">
{{ range $posts }}
{{ partial "components/post-card.html" (dict "Page" . "Root" $) }}
{{ else }}
<p class="text-xs text-muted">
No posts yet. Add some under <code>content/blog</code>.
</p>
{{ end }}
</div>
<div class="mt-3 flex justify-end">
<a
href="{{ "/blog/" | relURL }}"
class="btn-primary btn-primary-sm inline-flex items-center gap-2"
>
<span>View all posts</span>
<i class="fa-solid fa-arrow-right-long text-[0.8rem]"></i>
</a>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,43 @@
{{- $home := .Site.Params.home -}}
{{ if ne $home.showFeaturedProjects false }}
<div class="space-y-3 animate-fade-up">
<div class="space-y-1">
<h2 class="heading-section">
{{ default "Selected work" $home.projectsTitle }}
</h2>
{{ with $home.projectsSubtitle }}
<p class="text-xs text-muted">
{{ . }}
</p>
{{ end }}
</div>
{{ $limit := cond (gt (int $home.featuredProjectsLimit) 0) (int $home.featuredProjectsLimit) 3 }}
{{ $allProjects := where .Site.RegularPages "Section" "projects" }}
{{ $featured := where $allProjects "Params.featured" true }}
{{ if not (gt (len $featured) 0) }}
{{ $featured = $allProjects }}
{{ end }}
{{ $list := first $limit $featured }}
<div class="grid gap-4 md:grid-cols-2">
{{ range $list }}
{{ partial "components/project-card.html" (dict "Page" . "Root" $) }}
{{ else }}
<p class="text-xs text-muted">
No projects yet. Add some under <code>content/projects</code>.
</p>
{{ end }}
</div>
<div class="mt-3 flex justify-end">
<a
href="{{ "/projects/" | relURL }}"
class="btn-primary btn-primary-sm inline-flex items-center gap-2"
>
<span>View all projects</span>
<i class="fa-solid fa-arrow-right-long text-[0.8rem]"></i>
</a>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,59 @@
{{- $home := .Site.Params.home -}}
{{- $hero := .Site.Params.hero -}}
{{- $techMain := $home.tech -}}
{{- $techReverse := $home.techReverse -}}
{{- $variant := $home.techVariant | default "wide" -}}
{{ with $techMain }}
<div class="space-y-5 animate-fade-up">
<div class="space-y-2">
<h3 class="heading-section text-base">
{{ default "What I Work With" $hero.techMarqueeLabel }}
</h3>
<div class="tech-strip {{ if eq $variant "compact" }}tech-strip--compact{{ else }}tech-strip--wide{{ end }}">
<!-- primary row (always rendered) -->
<div class="tech-strip-track tech-strip-track--primary">
{{ range . }}
<div class="tech-strip-item">
{{ with .icon }}
<i class="{{ . }} tech-icon"></i>
{{ end }}
<span class="font-medium">{{ .label }}</span>
</div>
{{ end }}
{{ range . }}
<div class="tech-strip-item" aria-hidden="true">
{{ with .icon }}
<i class="{{ . }} tech-icon"></i>
{{ end }}
<span class="font-medium">{{ .label }}</span>
</div>
{{ end }}
</div>
<!-- secondary row, reverse (only if techReverse is defined) -->
{{ with $techReverse }}
<div class="tech-strip-track tech-strip-track--secondary">
{{ range . }}
<div class="tech-strip-item">
{{ with .icon }}
<i class="{{ . }} tech-icon"></i>
{{ end }}
<span class="font-medium">{{ .label }}</span>
</div>
{{ end }}
{{ range . }}
<div class="tech-strip-item" aria-hidden="true">
{{ with .icon }}
<i class="{{ . }} tech-icon"></i>
{{ end }}
<span class="font-medium">{{ .label }}</span>
</div>
{{ end }}
</div>
{{ end }}
</div>
</div>
</div>
{{ end }}

View File

@@ -0,0 +1,7 @@
{{- with .Description -}}
<meta name="description" content="{{ . }}" />
{{- else -}}
<meta name="description" content="{{ .Site.Params.description }}" />
{{- end }} {{- with .Site.Params.author }}
<meta name="author" content="{{ . }}" />
{{- end }}

View File

@@ -0,0 +1,59 @@
<div class="search-overlay" data-search-overlay>
<div class="search-overlay-backdrop" data-search-close></div>
<div class="search-panel">
<div class="search-panel-header">
<div class="search-input-wrap">
<i class="fa-solid fa-magnifying-glass text-[0.8rem] text-muted"></i>
<input
type="search"
class="search-input"
placeholder="Search posts..."
autocomplete="off"
data-search-input
/>
</div>
<button
type="button"
class="search-close"
data-search-close
aria-label="Close search"
>
<i class="fa-solid fa-xmark text-[0.8rem]"></i>
</button>
</div>
<div class="search-panel-body">
<div class="search-results" data-search-results>
<div class="search-empty-state">
<div class="search-empty-icon">
<i class="fa-solid fa-magnifying-glass text-[1rem]"></i>
</div>
<p class="search-empty-title">Start searching</p>
<p class="search-empty-subtitle">
Enter keywords to search articles.
</p>
</div>
</div>
<div class="search-footer-hints">
<div class="search-hint-key">
<span>↑↓</span>
<span>Navigate</span>
</div>
<div class="search-hint-key">
<span></span>
<span>Select</span>
</div>
<div class="search-hint-key">
<span>ESC</span>
<span>Close</span>
</div>
<div class="search-hint-key search-hint-right">
<span>Ctrl</span><span>K</span>
<span>Shortcut</span>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
{{ define "main" }}
<section class="layout-page">
<div class="page-int section-stack">
<header class="space-y-2">
<h1 class="heading-page text-2xl sm:text-3xl">Projects</h1>
{{ with .Site.Params.projectsIntro }}
<p class="max-w-xl text-sm text-muted">
{{ . }}
</p>
{{ else }}
<p class="max-w-xl text-sm text-muted">
A selection of things Ive been building themes, tools, and experiments.
</p>
{{ end }}
</header>
<!-- Cards -->
{{/* you can tweak sorting if you want; this is newest first */}}
{{ $paginator := .Paginate (.Pages.ByDate.Reverse) }}
<div class="grid gap-4 md:grid-cols-2">
{{ range $paginator.Pages }}
{{ partial "components/project-card.html" (dict "Page" . "Root" $) }}
{{ end }}
</div>
<!-- Pagination -->
{{ if gt $paginator.TotalPages 1 }}
<nav class="mt-6 flex items-center justify-between text-xs text-muted">
{{ if $paginator.HasPrev }}
<a href="{{ $paginator.Prev.URL }}" class="link-underline">
← Newer projects
</a>
{{ else }}
<span></span>
{{ end }}
{{ if $paginator.HasNext }}
<a href="{{ $paginator.Next.URL }}" class="link-underline">
Older projects →
</a>
{{ else }}
<span></span>
{{ end }}
</nav>
{{ end }}
</div>
</section>
{{ end }}