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

33
themes/minimal-black/.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
# Hugo
public/
resources/
.hugo_build.lock
hugo_stats.json
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
# IDE & Editor
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Build artifacts
*.log
*.tmp
*.temp
# Backup files
*.backup
*.bak
# OS files
Thumbs.db
Desktop.ini

View File

@@ -0,0 +1,111 @@
# Changelog
All notable changes to the Minimal Black theme will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Initial theme release
- Dark and light mode with automatic switching
- True black dark mode with purple accents
- Responsive design for mobile, tablet, and desktop
- Built-in search functionality with keyboard shortcuts (Ctrl/Cmd+K)
- Table of contents for blog posts with active section tracking
- Syntax highlighting with copy button and language labels
- Multiple page layouts: home, blog, projects, about, about-alternative
- Mermaid diagram support
- Gallery shortcode with lightbox
- GitHub-style alerts (note, tip, warning, danger, important)
- Analytics support (Google Analytics, Plausible, Umami, Fathom)
- Font Awesome and Devicon icon support
- Technology stack marquee on home page
- Project portfolio with featured projects
- Blog with tags and categories
- SEO optimized meta tags
- RSS feed generation
- JSON output for search indexing
- Modular CSS architecture
- Configurable stats and skills for about-alternative page
- Favicon and touch icon support
- Social links configuration
- Hero section with customizable badge, title, and CTAs
- "Now" section for quick facts
- Floating action dock
- Mobile-friendly navigation
- Theme toggle button
- Custom 404 page
- Markdown enhancements:
- Enhanced blockquotes with gradient backgrounds
- Improved list styling with hover effects
- Task list support with checkboxes
- Definition lists
- Footnotes
- Table styling with responsive wrapper
- Image captions (optional)
- External link indicators
- Heading anchor links
### Changed
- N/A (initial release)
### Deprecated
- N/A (initial release)
### Removed
- N/A (initial release)
### Fixed
- N/A (initial release)
### Security
- N/A (initial release)
## Release Notes
### Version 1.0.0 (Unreleased)
Initial release of Minimal Black theme for Hugo. A minimal, dark-mode first theme designed for developers, designers, and writers who value simplicity and performance.
**Key Features:**
- ⚡ Fast and lightweight
- 🎨 Beautiful dark mode with true black backgrounds
- 📱 Fully responsive design
- 🔍 Built-in search
- 💻 Excellent code highlighting
- 📚 Comprehensive documentation
---
## Version History Format
Each version entry should follow this structure:
```markdown
## [X.Y.Z] - YYYY-MM-DD
### Added
- New features
### Changed
- Changes in existing functionality
### Deprecated
- Soon-to-be removed features
### Removed
- Removed features
### Fixed
- Bug fixes
### Security
- Security patches
```
---
[Unreleased]: https://gitlab.com/jimchr12/hugo-minimal-black/-/compare/v1.0.0...main
[1.0.0]: https://gitlab.com/jimchr12/hugo-minimal-black/-/releases/v1.0.0

View File

@@ -0,0 +1,249 @@
# Contributing to Minimal Black
Thank you for your interest in contributing to Minimal Black! This document provides guidelines and instructions for contributing to the theme.
## Code of Conduct
By participating in this project, you agree to maintain a respectful and collaborative environment. Be kind, be professional, and be constructive in your feedback.
## How Can I Contribute?
### Reporting Bugs
Before creating a bug report, please check the [existing issues](https://gitlab.com/jimchr12/hugo-minimal-black/-/issues) to avoid duplicates.
When creating a bug report, include:
- **Clear title** — Descriptive and specific
- **Steps to reproduce** — Minimal steps needed to reproduce the issue
- **Expected behavior** — What you expected to happen
- **Actual behavior** — What actually happened
- **Screenshots** — If applicable
- **Environment:**
- Hugo version (`hugo version`)
- Operating system
- Browser (if frontend issue)
- Node.js version (`node --version`)
### Suggesting Enhancements
Enhancement suggestions are welcome! Please:
1. **Check existing issues** — Your idea might already be proposed
2. **Provide clear use case** — Explain why this would be useful
3. **Consider alternatives** — Mention any alternative solutions you've considered
4. **Keep it minimal** — The theme prioritizes simplicity
### Pull Requests
1. **Fork the repository** and create your branch from `main`
2. **Make your changes** following the code style guidelines below
3. **Test your changes** thoroughly
4. **Update documentation** if you're changing functionality
5. **Commit with clear messages** following the commit conventions
6. **Submit a pull request** with a comprehensive description
## Development Setup
### Prerequisites
- Hugo Extended v0.120.0+
- Node.js 18+
- npm or yarn
### Local Development
1. **Clone your fork:**
```bash
git clone https://gitlab.com/YOUR-USERNAME/hugo-minimal-black.git
cd hugo-minimal-black
```
2. **Install dependencies:**
```bash
npm install
```
3. **Run the example site:**
```bash
cd exampleSite
hugo server -D --themesDir ../..
```
4. **Build CSS** (if modifying styles):
```bash
npx tailwindcss -i ./assets/css/main.css -o ./static/css/main.css
```
### Making Changes
#### CSS/Styling
- CSS files are in `assets/css/`
- The theme uses Tailwind CSS with a modular structure
- See [CSS-STRUCTURE.md](CSS-STRUCTURE.md) for architecture details
- Follow the existing pattern: one component per file
- Use CSS custom properties for theme colors
- Ensure changes work in both light and dark modes
#### HTML/Templates
- Templates are in `layouts/`
- Follow Hugo's template best practices
- Use semantic HTML5 elements
- Test responsiveness on mobile, tablet, and desktop
#### JavaScript
- Keep JavaScript minimal
- Use vanilla JavaScript (no frameworks)
- Ensure compatibility with all modern browsers
- Add comments for complex logic
- Test with JavaScript disabled for core functionality
### HTML/Templates
- **2 spaces** for indentation
- **Semantic HTML** — Use appropriate elements
- **Accessibility** — Include ARIA labels where needed
- **Hugo conventions** — Follow Hugo's best practices
```html
{{ define "main" }}
<section class="layout-page">
<article class="about-page">
<h1>{{ .Title }}</h1>
{{ .Content }}
</article>
</section>
{{ end }}
```
### CSS
- **2 spaces** for indentation
- **BEM-inspired** naming for custom classes
- **Mobile-first** responsive design
- **CSS custom properties** for theme values
- **Comments** for complex styles
```css
/* Component name */
.component-name {
property: value;
}
/* Component variant */
.component-name--variant {
property: value;
}
/* Component element */
.component-name__element {
property: value;
}
```
### JavaScript
- **2 spaces** for indentation
- **camelCase** for variables and functions
- **Descriptive names** — Avoid abbreviations
- **Comments** for non-obvious code
- **Modern syntax** — Use ES6+ features
```javascript
function handleSearchInput(event) {
const query = event.target.value;
const results = filterPages(query);
renderResults(results);
}
```
## Commit Messages
Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification:
```
type(scope): subject
body (optional)
footer (optional)
```
### Types
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Code style changes (formatting, etc.)
- `refactor`: Code refactoring
- `perf`: Performance improvements
- `test`: Adding or updating tests
- `chore`: Maintenance tasks
### Examples
```
feat(search): add keyboard navigation to search results
fix(toc): correct active link highlighting on scroll
docs(readme): update installation instructions
style(css): reorganize markdown styles into modules
refactor(cards): simplify project card component
perf(images): optimize image loading with lazy loading
```
## Testing Checklist
Before submitting a pull request, ensure:
- [ ] Hugo builds without errors (`hugo`)
- [ ] No console errors in browser
- [ ] Works in latest Chrome, Firefox, Safari
- [ ] Responsive on mobile, tablet, desktop
- [ ] Dark and light modes both work
- [ ] Accessibility: Keyboard navigation works
- [ ] Performance: Lighthouse score >90
- [ ] Documentation updated (if applicable)
- [ ] No breaking changes (or clearly documented)
## File Structure
```
minimal-black/
├── archetypes/ # Content templates
├── assets/
│ └── css/ # Modular CSS files
├── layouts/
│ ├── _default/ # Default layouts
│ ├── partials/ # Reusable components
│ └── shortcodes/ # Custom shortcodes
├── static/
│ └── js/ # JavaScript files
├── exampleSite/ # Example site for testing
├── images/ # Theme screenshots
├── CSS-STRUCTURE.md # CSS architecture docs
├── CONTRIBUTING.md # This file
├── LICENSE # MIT License
├── README.md # Main documentation
└── theme.toml # Theme metadata
```
## Questions?
- **Issues:** [GitLab Issues](https://gitlab.com/jimchr12/hugo-minimal-black/-/issues)
- **Discussions:** [GitLab Discussions](https://gitlab.com/jimchr12/hugo-minimal-black/-/issues)
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
---
Thank you for contributing to Minimal Black! 🎉

View File

@@ -0,0 +1,106 @@
# CSS Architecture
The Minimal Black theme uses a modular CSS architecture for better maintainability and organization.
## Directory Structure
```
assets/css/
├── main.css # Entry point (imports all modules)
├── base.css # Tailwind imports & theme variables
├── utilities.css # Utility classes & animations
├── responsive.css # Global responsive styles
├── components/
│ ├── dock.css # Floating action dock
│ ├── cards.css # Card variants (home, project, post, CTA)
│ ├── navigation.css # Header, footer, nav links
│ ├── search.css # Search overlay & results
│ └── tech-marquee.css # Technology carousel
├── content/
│ ├── markdown.css # Typography, blockquotes, lists, code
│ └── toc.css # Table of contents sidebar
└── pages/
├── about.css # About page with timeline
└── about-alternative.css # Alternative about page layout
```
## Module Descriptions
### Base Layer
- **base.css**: Tailwind directives, CSS custom properties for theming (light/dark mode), and base body styles
### Utilities
- **utilities.css**: Color utilities, animation classes, and helper classes used throughout the theme
### Components
Reusable UI components that appear across multiple pages:
- **dock.css**: Floating action button dock with expandable panel
- **cards.css**: All card variants including home cards, project/post cards, CTA cards, and badges
- **navigation.css**: Page layouts, header, footer, navigation links, and theme toggle
- **search.css**: Search modal overlay, search results, empty states, and keyboard hints
- **tech-marquee.css**: Animated technology/skills carousel component
### Content
Styles specific to content rendering:
- **markdown.css**: Complete markdown styling including typography, blockquotes, lists, code blocks, tables, GitHub-style alerts, and syntax highlighting
- **toc.css**: Table of contents sidebar with sticky positioning and active link tracking
### Pages
Page-specific styles:
- **about.css**: Standard about page with timeline visualization
- **about-alternative.css**: Alternative about layout with sidebar profile card and stats
### Responsive
- **responsive.css**: All media queries and mobile optimizations for global components
## Import Order
The import order in `main.css` is important:
1. Base styles (Tailwind + variables)
2. Utilities (available to all components)
3. Components (dock → cards → navigation → search → tech)
4. Content styles (markdown → TOC)
5. Page-specific styles
6. Responsive overrides
## Development Guidelines
### Adding New Styles
- Component styles → `components/`
- Content/typography → `content/`
- Page-specific → `pages/`
- Utilities → `utilities.css`
- Theme variables → `base.css`
### Modifying Existing Styles
Each file is focused on a specific concern. Find the relevant module and edit it directly. Hugo will automatically rebuild the combined CSS.
### Theme Colors
All theme colors are defined as CSS custom properties in `base.css`:
```css
--color-bg /* Background */
--color-surface /* Card/surface backgrounds */
--color-text /* Primary text */
--color-text-muted /* Secondary text */
--color-border /* Borders */
--color-accent /* Primary accent color */
```
These variables automatically switch between light and dark themes.
### Responsive Breakpoints
- Mobile: `max-width: 640px`
- Tablet: `641px - 1023px`
- Desktop: `min-width: 1024px`
- Large Desktop: `min-width: 1536px`
- XL Desktop: `min-width: 1920px`
## Build Process
Hugo processes `main.css` through:
1. Resolves `@import` statements
2. Processes Tailwind directives (@tailwind, @apply)
3. Minifies for production
4. Outputs to `public/css/main.css`
The modular structure is development-only; users receive a single optimized CSS file.

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Jim Christopoulos
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,457 @@
# Minimal Black
A minimal, dark-mode first Hugo theme with true black backgrounds, purple accents, and comprehensive content styling. Perfect for developers, writers and everyone in general who want a clean, fast, and beautiful personal site.
![Minimal Black Theme](https://gitlab.com/jimchr12/hugo-minimal-black/-/raw/main/images/screenshot.png?ref_type=heads)
![Minimal Black Theme About Page](https://gitlab.com/jimchr12/hugo-minimal-black/-/raw/main/images/about.png?ref_type=heads)
![Minimal Black Theme About Alternative Page](https://gitlab.com/jimchr12/hugo-minimal-black/-/raw/main/images/about-alt.png?ref_type=heads)
## Features
-**Dark & Light Mode** — Auto-switching themes with manual toggle
- 🎨 **True Black Dark Mode** — OLED-friendly with purple (`#a855f7`) accents
- 📱 **Fully Responsive** — Mobile-first design that works on all devices
- 🔍 **Built-in Search** — Fast client-side search with keyboard shortcuts (Ctrl/Cmd+K)
- 📑 **Table of Contents** — Auto-generated TOC for blog posts with active section tracking
- 💻 **Beautiful Code Blocks** — Syntax highlighting with copy button and language labels
- 🎯 **Multiple Layouts** — Home, blog, projects, and two about page variants
- 📊 **Mermaid Diagrams** — Native support for flowcharts and diagrams
- 🖼️ **Image Gallery** — Gallery shortcode with lightbox functionality
- 🏷️ **GitHub-Style Alerts** — Support for note, tip, warning, danger, and important callouts
- 📈 **Analytics Ready** — Built-in support for Google Analytics, Plausible, Umami, and Fathom
-**Performance Optimized** — Minimal JavaScript, CSS, and fast load times
- 🎭 **Icon Support** — Font Awesome and Devicon integration
## Demo
**Live Demo:** [minimal-black-demo.netlify.app](https://minimal-black-demo.netlify.app)
## Quick Start
### Prerequisites
- Hugo Extended v0.120.0 or higher
- Node.js and npm (for Tailwind CSS compilation)
### Installation
1. **Create a new Hugo site** (or use existing):
```bash
hugo new site my-site
cd my-site
```
2. **Install the theme** using Git submodule:
```bash
git init
git submodule add https://gitlab.com/jimchr12/hugo-minimal-black.git themes/minimal-black
```
Or clone it directly:
```bash
git clone https://gitlab.com/jimchr12/hugo-minimal-black.git themes/minimal-black
```
3. **Install Node dependencies** (for Tailwind CSS):
```bash
cd themes/minimal-black
npm install
cd ../..
```
4. **Configure your site** — Copy the example configuration:
```bash
cp themes/minimal-black/exampleSite/hugo.toml ./hugo.toml
```
5. **Start Hugo server**:
```bash
hugo server -D
```
6. **Visit** `http://localhost:1313` in your browser.
## Configuration
### Basic Configuration
Edit your `hugo.toml` file:
```toml
baseURL = 'https://yoursite.com/'
languageCode = 'en-us'
title = 'Your Name'
theme = 'minimal-black'
[params]
brand = "Your Name"
# Favicon (place files in /static/)
favicon = "favicon.svg"
appleTouchIcon = "apple-touch-icon.png"
# Theme Configuration
[params.theme]
defaultTheme = "dark" # "light", "dark", or "system"
# Hero Section
[params.hero]
badge = "Software Engineer"
title = "Hi, I'm Your Name."
role = "Building minimal, fast web experiences."
summary = "Brief description of what you do."
avatar = "images/avatar.jpg" # Optional
location = "City, Country"
[params.hero.primary]
label = "View projects"
href = "/projects/"
[params.hero.secondary]
label = "Read the blog"
href = "/blog/"
# Social Links
[[params.social]]
label = "GitHub"
url = "https://github.com/yourusername"
icon = "fa-brands fa-github"
[[params.social]]
label = "LinkedIn"
url = "https://linkedin.com/in/yourusername"
icon = "fa-brands fa-linkedin-in"
[menu]
[[menu.main]]
name = "Home"
url = "/"
weight = 1
[[menu.main]]
name = "About"
url = "/about/"
weight = 2
[[menu.main]]
name = "Projects"
url = "/projects/"
weight = 3
[[menu.main]]
name = "Blog"
url = "/blog/"
weight = 4
[markup]
[markup.tableOfContents]
startLevel = 2
endLevel = 4
[markup.goldmark.renderer]
unsafe = true
[markup.highlight]
codeFences = true
guessSyntax = true
style = "monokai"
```
### Advanced Configuration
#### Analytics
Google Analytics (GA4):
```toml
[params.analytics]
googleAnalytics = "G-XXXXXXXXXX"
```
Plausible Analytics:
```toml
[params.analytics.plausible]
enabled = true
domain = "yourdomain.com"
```
Umami Analytics:
```toml
[params.analytics.umami]
enabled = true
scriptUrl = "https://analytics.yourdomain.com/script.js"
websiteId = "your-website-id"
```
#### Technology Stack Display
Show your tech stack on the home page:
```toml
[params.home]
[[params.home.tech]]
label = "Python"
icon = "devicon-python-plain"
[[params.home.tech]]
label = "Docker"
icon = "devicon-docker-plain"
```
#### About Page Customization
For the alternative about page layout:
```toml
[params.about.alt]
# Stats in profile card
[[params.about.alt.stats]]
value = "5+"
label = "Years Coding"
[[params.about.alt.stats]]
value = "20+"
label = "Projects"
# Skills with icons
[[params.about.alt.skills]]
label = "JavaScript"
icon = "devicon-javascript-plain"
```
## Content Organization
### Creating Content
**Blog Post:**
```bash
hugo new blog/my-first-post.md
```
```markdown
+++
title = "My First Post"
date = "2025-01-01"
author = "Your Name"
tags = ["hugo", "web development"]
categories = ["tutorials"]
draft = false
+++
Your content here...
```
**Project:**
```bash
hugo new projects/my-project.md
```
```markdown
+++
title = "My Project"
date = "2025-01-01"
description = "Brief project description"
github = "https://github.com/username/project"
demo = "https://project-demo.com"
tags = ["react", "typescript"]
featured = true
+++
Project details...
```
### About Page
Create `content/about.md`:
```markdown
+++
title = "About Me"
subtitle = "Software Engineer | Open Source Contributor"
layout = "about" # or "about-alternative"
+++
Your introduction here.
---
**Job Title** — [Company](https://company.com)
*Dates • Location*
Description of role...
---
**Another Role** — Company
*Dates • Location*
Description...
```
The `---` separators create timeline entries in the standard about layout, or experience cards in the alternative layout.
## Shortcodes
### Gallery
Create image galleries with lightbox:
```markdown
{{< gallery >}}
![Alt text](./images/screenshot.png)
![Alt text](./images/about.png)
![Alt text](./images/about-alt.png)
{{< /gallery >}}
```
### Alert
Create GitHub-style callouts:
```markdown
{{< alert type="note" >}}
This is a note alert.
{{< /alert >}}
{{< alert type="warning" >}}
This is a warning alert.
{{< /alert >}}
```
Available types: `note`, `tip`, `important`, `warning`, `danger`
### Mermaid Diagrams
````markdown
```mermaid
graph TD
A[Start] --> B[Process]
B --> C[End]
```
````
## Customization
### Colors
Theme colors are defined in `assets/css/base.css`:
```css
:root {
--color-bg: #f9fafb; /* Light background */
--color-accent: #a855f7; /* Purple accent */
}
html[data-theme="dark"] {
--color-bg: #000000; /* True black */
--color-accent: #c084fc; /* Lighter purple */
}
```
### CSS Architecture
The theme uses a modular CSS structure:
```
assets/css/
├── main.css # Imports all modules
├── base.css # Tailwind & variables
├── utilities.css # Utility classes
├── components/ # Reusable components
├── content/ # Content styling
├── pages/ # Page-specific styles
└── responsive.css # Media queries
```
See [CSS-STRUCTURE.md](CSS-STRUCTURE.md) for details.
### Adding Custom CSS
Create `assets/css/custom.css` in your site root and import it in your config:
```toml
[params]
customCSS = ["css/custom.css"]
```
## Deployment
### Netlify
1. Push your site to GitHub
2. Connect to Netlify
3. Build command: `hugo --minify`
4. Publish directory: `public`
5. Environment variables:
```
HUGO_VERSION = 0.120.0
NODE_VERSION = 18
```
## Browser Support
- Chrome/Edge (last 2 versions)
- Firefox (last 2 versions)
- Safari (last 2 versions)
- Mobile browsers (iOS Safari, Chrome Mobile)
## Troubleshooting
### CSS not loading
Ensure Tailwind dependencies are installed:
```bash
cd themes/minimal-black
npm install
```
Also ensure css is compiled:
```bash
npx tailwindcss -i ./assets/css/main.css -o ./static/css/main.css
```
### Search not working
Check that `[outputs]` includes JSON in `hugo.toml`:
```toml
[outputs]
home = ["HTML", "RSS", "JSON"]
```
### Icons not showing
Verify icon library is enabled:
```toml
[params.icons]
useFontAwesome = true
useDevicon = true
```
## Contributing
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Submit a pull request
## License
This theme is released under the MIT License. See [LICENSE](LICENSE) for details.
## Credits
- Built with [Hugo](https://gohugo.io/)
- Styled with [Tailwind CSS](https://tailwindcss.com/)
- Icons from [Font Awesome](https://fontawesome.com/) and [Devicon](https://devicon.dev/)
- Typography plugin by [@tailwindcss/typography](https://github.com/tailwindlabs/tailwindcss-typography)
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history and updates.
---
**Made with ❤️ by [Jim Christopoulos](https://jimchristopoulos.com/)**
If you find this theme useful, consider giving it a ⭐ and/or supporting its development!
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/jimchr)

View File

@@ -0,0 +1,35 @@
/* ==========================================================================
BASE STYLES
Tailwind imports, CSS variables, theme colors, and base styles
========================================================================== */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Theme Variables */
:root {
/* LIGHT THEME */
--color-bg: #f9fafb;
--color-surface: #ffffff;
--color-text: #111827;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
--color-accent: #a855f7;
}
html[data-theme="dark"] {
/* DARK THEME */
--color-bg: #000000; /* TRUE BLACK */
--color-surface: #0a0a0a;
--color-text: #f9fafb;
--color-text-muted: #9ca3af;
--color-border: #27272a;
--color-accent: #c084fc;
}
/* Base Styles */
body {
background: var(--color-bg);
color: var(--color-text);
}

View File

@@ -0,0 +1,271 @@
/* ==========================================================================
CARD COMPONENTS
Home cards, project cards, post cards, CTA cards, badges
========================================================================== */
.card-home {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 0.5rem;
background-color: color-mix(in srgb, var(--color-surface) 96%, transparent);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
}
.card-home--project {
position: relative;
overflow: hidden;
transition:
transform 0.18s ease-out,
box-shadow 0.18s ease-out,
border-color 0.18s ease-out,
background-color 0.18s ease-out;
}
.card-home--project::after {
content: "";
position: absolute;
inset: -40%;
background: radial-gradient(
circle at 120% 50%,
color-mix(in srgb, var(--color-accent) 70%, transparent),
transparent 60%
);
opacity: 0;
transform: translateX(-8px);
transition:
opacity 0.22s ease-out,
transform 0.22s ease-out;
pointer-events: none;
}
.card-home--project:hover {
transform: translateY(-4px) translateX(3px);
border-color: color-mix(in srgb, var(--color-accent) 40%, var(--color-border));
box-shadow:
0 18px 40px rgba(0, 0, 0, 0.7),
16px 0 38px rgba(124, 58, 237, 0.45); /* purple-ish accent on right */
background-color: color-mix(in srgb, var(--color-surface) 99%, transparent);
}
.card-home--project:hover::after {
opacity: 0.22;
transform: translateX(0);
}
.card-home-body {
display: flex;
flex-direction: column;
gap: 0.5rem;
text-decoration: none;
color: inherit;
}
.card-home--post {
background-color: color-mix(in srgb, var(--color-surface) 94%, transparent);
}
.card-home-header {
display: flex;
align-items: flex-start;
gap: 0.6rem;
}
.card-home-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.1rem;
height: 2.1rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-bg) 92%, transparent);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
font-size: 0.85rem;
}
.card-badge {
margin-left: 0.25rem;
padding: 0.05rem 0.35rem;
border-radius: 999px;
background-color: color-mix(in srgb, var(--color-accent) 22%, transparent);
color: var(--color-accent);
font-size: 0.6rem;
font-weight: 600;
}
.card-home-footer {
margin-top: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
justify-content: space-between;
}
.card-home-footer--buttons {
padding-top: 0.4rem;
border-top: 1px solid color-mix(in srgb, var(--color-border) 80%, transparent);
}
.card-tag-row {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.card-tag-pill {
padding: 0.1rem 0.45rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--color-border) 80%, transparent);
background-color: color-mix(in srgb, var(--color-bg) 92%, transparent);
font-size: 0.65rem;
color: var(--color-text-muted);
}
.card-cta {
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.7rem;
color: var(--color-text-muted);
text-decoration: none;
padding: 0.25rem 0.6rem;
border-radius: 999px;
border: 1px solid transparent;
transition:
color 0.15s ease-out,
border-color 0.15s ease-out,
background-color 0.15s ease-out,
transform 0.15s ease-out;
}
.card-cta-btn {
display: inline-flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.6rem;
border-radius: 8px;
font-size: 0.68rem;
color: var(--color-text-muted);
background-color: color-mix(in srgb, var(--color-bg) 92%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 80%, transparent);
text-decoration: none;
transition:
color 0.15s ease-out,
background-color 0.15s ease-out,
border-color 0.15s ease-out,
transform 0.15s ease-out,
box-shadow 0.15s ease-out;
}
.card-cta-btn:hover {
color: var(--color-accent);
background-color: color-mix(in srgb, var(--color-surface) 96%, transparent);
border-color: color-mix(in srgb, var(--color-accent) 50%, transparent);
transform: translateY(-1px);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
}
.card-cta-repo {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.25rem 0.7rem;
border-radius: 999px;
border: 1px solid color-mix(in srgb, var(--color-border) 85%, transparent);
background-color: color-mix(in srgb, var(--color-bg) 94%, transparent);
font-size: 0.7rem;
color: var(--color-text-muted);
text-decoration: none;
transition:
color 0.15s ease-out,
border-color 0.15s ease-out,
background-color 0.15s ease-out,
transform 0.15s ease-out,
box-shadow 0.15s ease-out;
}
.card-cta-repo:hover {
color: var(--color-accent);
border-color: color-mix(in srgb, var(--color-accent) 55%, transparent);
background-color: color-mix(in srgb, var(--color-surface) 96%, transparent);
transform: translateY(-1px);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.5);
}
.card-cta:hover {
color: var(--color-accent);
border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
background-color: color-mix(in srgb, var(--color-surface) 94%, transparent);
transform: translateY(-1px);
}
.card-home--post {
position: relative;
overflow: hidden;
background-color: color-mix(in srgb, var(--color-surface) 94%, transparent);
border-left-width: 2px;
border-left-color: color-mix(in srgb, var(--color-border) 80%, transparent);
transition:
transform 0.18s ease-out,
box-shadow 0.18s ease-out,
border-color 0.18s ease-out,
background-color 0.18s ease-out;
}
.card-home--post::before {
content: "";
position: absolute;
left: -4px;
top: -20%;
bottom: -20%;
width: 10px;
background: linear-gradient(
to bottom,
transparent,
color-mix(in srgb, var(--color-accent) 70%, transparent),
transparent
);
opacity: 0;
transform: translateX(-6px);
transition:
opacity 0.22s ease-out,
transform 0.22s ease-out;
pointer-events: none;
}
.card-home--post:hover {
transform: translateY(-3px);
background-color: color-mix(in srgb, var(--color-surface) 98%, transparent);
border-left-color: color-mix(in srgb, var(--color-accent) 55%, var(--color-border));
box-shadow: 0 16px 34px rgba(0, 0, 0, 0.65);
}
.card-home--post:hover::before {
opacity: 0.55;
transform: translateX(0);
}
.card-home-icon--post {
background-color: color-mix(in srgb, var(--color-bg) 94%, transparent);
}
.card-badge--soft {
background-color: color-mix(in srgb, var(--color-accent) 16%, transparent);
color: color-mix(in srgb, var(--color-accent) 85%, white);
}
.card {
border-radius: 0.75rem;
transition: all 0.2s ease-out;
}
.card-pad {
padding: 1rem;
}
.card:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
}

View File

@@ -0,0 +1,134 @@
/* ==========================================================================
DOCK COMPONENT
Floating action dock with toggle and panel
========================================================================== */
@layer components {
.dock {
position: fixed;
right: 1.5rem;
bottom: 1.6rem;
z-index: 50;
}
.dock-inner {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.3rem 0.5rem;
border-radius: 9999px;
/* border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-surface) 92%, transparent);
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.55); */
backdrop-filter: blur(16px);
}
.dock-panel {
@apply p-1;
display: inline-flex;
align-items: center;
gap: 0.35rem;
max-width: 0;
opacity: 0;
transform: translateX(6px);
overflow: hidden;
transition: max-width 0.18s ease-out, opacity 0.18s ease-out,
transform 0.18s ease-out;
}
.dock--open .dock-panel {
max-width: 10rem;
opacity: 1;
transform: translateX(0);
}
.dock-divider {
width: 1px;
height: 1.4rem;
background: linear-gradient(
to bottom,
transparent,
color-mix(in srgb, var(--color-border) 80%, transparent),
transparent
);
}
.dock-action {
@apply flex items-center justify-center;
width: 2rem;
height: 2rem;
border-radius: 9999px;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-bg) 75%, transparent);
color: var(--color-text-muted);
cursor: pointer;
transition: background-color 0.16s ease-out, border-color 0.16s ease-out,
color 0.16s ease-out, transform 0.16s ease-out, box-shadow 0.16s ease-out;
}
.dock-action:hover {
border-color: var(--color-accent);
color: var(--color-accent);
transform: translateY(-1px);
/* box-shadow: 0 8px 26px rgba(0, 0, 0, 0.55); */
}
.dock-toggle {
@apply flex items-center justify-center;
width: 2.1rem;
height: 2.1rem;
border-radius: 9999px;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-bg) 85%, transparent);
color: var(--color-text-muted);
cursor: pointer;
transition: background-color 0.16s ease-out, border-color 0.16s ease-out,
transform 0.16s ease-out, box-shadow 0.16s ease-out;
}
.dock-toggle:hover {
border-color: var(--color-accent);
color: var(--color-accent);
transform: translateY(-1px);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.5);
}
.dock-toggle-dots {
position: relative;
display: inline-block;
width: 0.8rem;
height: 0.16rem;
border-radius: 999px;
background-color: var(--color-text-muted);
}
.dock-toggle-dots::before,
.dock-toggle-dots::after {
content: "";
position: absolute;
inset-y: 0;
width: 0.16rem;
border-radius: 999px;
background-color: var(--color-text-muted);
}
.dock-toggle-dots::before {
left: -0.18rem;
}
.dock-toggle-dots::after {
right: -0.18rem;
}
.dock--open .dock-toggle-dots,
.dock--open .dock-toggle-dots::before,
.dock--open .dock-toggle-dots::after {
background-color: var(--color-accent);
}
.btn-primary-sm {
font-size: 0.75rem;
padding: 0.35rem 0.9rem;
border-radius: 999px;
}
}

View File

@@ -0,0 +1,203 @@
/* ==========================================================================
LAYOUT & NAVIGATION
Page layouts, headers, footers, navigation links
========================================================================== */
.layout-page {
@apply mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:py-12;
}
.layout-page-tight {
@apply mx-auto max-w-6xl px-4 py-4 sm:px-6 lg:py-10;
}
.section-stack {
@apply space-y-10;
}
.section-stack--home > * + * {
margin-top: 1.75rem;
}
.section-stack > div + div.posts-section {
border-top: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
padding-top: 1.5rem;
}
/* Headings */
.heading-page {
@apply text-3xl font-semibold tracking-tight sm:text-4xl;
}
.heading-section {
@apply text-sm font-semibold tracking-tight;
}
.eyebrow {
@apply text-[0.65rem] uppercase tracking-[0.22em];
}
/* Cards */
.card {
@apply border border-border bg-surface rounded-xl shadow-sm;
}
.card-pad {
@apply p-4 sm:p-5;
}
.card-hover {
@apply transition-transform transition-shadow duration-150 ease-out;
}
.card-hover:hover {
@apply shadow-lg;
transform: translateY(-1px);
border-color: var(--color-accent);
}
/* Buttons */
.btn-primary {
@apply inline-flex items-center rounded-full bg-accent px-4 py-2 text-xs font-medium text-white shadow-sm transition-transform duration-150 ease-out;
}
.btn-primary:hover {
transform: translateY(-1px);
}
.btn-ghost {
@apply inline-flex items-center text-xs font-medium text-muted;
}
.nav-shell {
@apply rounded-full border border-border shadow-md backdrop-blur p-3;
background-color: color-mix(in srgb, var(--color-surface) 82%, transparent);
}
.nav-shell::before {
content: "";
position: absolute;
inset-inline: 0;
top: 0;
bottom: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
color-mix(in srgb, var(--color-accent) 80%, transparent),
transparent
);
opacity: 0.8;
pointer-events: none;
}
.nav-inner {
@apply flex items-center justify-between px-4 py-2 sm:px-5 sm:py-2.5;
}
/* Brand logo badge (fallback) */
.logo-badge {
@apply flex h-8 w-8 items-center justify-center rounded-full bg-accent text-[0.65rem] font-semibold text-white;
}
/* Nav links */
.nav-link {
@apply text-[0.78rem] font-medium text-muted;
}
.nav-link:hover {
color: var(--color-accent);
}
/* Theme toggle */
.theme-toggle {
@apply inline-flex h-8 w-8 items-center justify-center rounded-full border border-border bg-bg text-muted shadow-sm text-[0.7rem];
transition: background-color 0.15s ease-out, color 0.15s ease-out,
transform 0.15s ease-out;
}
.theme-toggle:hover {
@apply text-accent;
transform: translateY(-1px);
}
html[data-theme="dark"] .theme-toggle {
background-color: var(--color-surface);
}
/* Fancy underline link */
.link-underline {
position: relative;
display: inline-block;
}
.link-underline::after {
content: "";
position: absolute;
left: 0;
bottom: -0.15rem;
width: 0;
height: 1px;
background-color: var(--color-accent);
transition: width 0.16s ease-out;
}
.link-underline:hover::after {
width: 100%;
}
/* Footer shell */
.footer-shell {
@apply rounded-2xl border border-border shadow-md backdrop-blur;
position: relative;
overflow: hidden;
background-color: color-mix(in srgb, var(--color-surface) 86%, transparent);
}
.footer-shell::before {
content: "";
position: absolute;
inset-inline: 0;
top: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
color-mix(in srgb, var(--color-accent) 80%, transparent),
transparent
);
opacity: 0.8;
pointer-events: none;
}
.footer-inner {
@apply flex flex-col gap-3 px-4 py-3 text-[0.72rem] sm:flex-row sm:items-center sm:justify-between sm:px-5;
}
.footer-links {
@apply flex flex-wrap items-center gap-3;
}
.footer-link {
@apply inline-flex items-center gap-1 text-[0.75rem] font-medium text-muted transition-all duration-150 ease-out;
}
.footer-link:hover {
color: var(--color-accent);
}
.footer-small {
@apply text-[0.72rem] text-muted leading-relaxed;
}
.footer-float:hover {
color: var(--color-accent);
transform: translateY(-1px);
}
.footer-float:hover i {
filter: drop-shadow(
0 0 2px color-mix(in srgb, var(--color-accent) 60%, transparent)
);
}

View File

@@ -0,0 +1,289 @@
/* ==========================================================================
SEARCH OVERLAY
Search modal, search results, empty states
========================================================================== */
.search-overlay {
position: fixed;
inset: 0;
z-index: 40;
display: none;
align-items: center;
justify-content: center;
opacity: 0;
pointer-events: none;
transition: opacity 0.18s ease-out;
}
.search-overlay--open {
opacity: 1;
display: block;
pointer-events: auto;
}
.search-overlay-backdrop {
position: absolute;
inset: 0;
background: radial-gradient(
circle at top,
rgba(0, 0, 0, 0.4),
transparent 55%
),
rgba(0, 0, 0, 0.7);
backdrop-filter: blur(12px);
}
.search-panel {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
max-width: 36rem;
margin-inline: 1.5rem;
border-radius: 1.1rem;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-bg) 96%, transparent);
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.85);
padding: 0.8rem 0.9rem 0.6rem;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.search-overlay--open .search-panel {
animation: search-panel-in 0.18s ease-out;
}
.search-overlay-backdrop {
position: absolute;
inset: 0;
background: radial-gradient(
circle at top,
rgba(0, 0, 0, 0.4),
transparent 55%
),
rgba(0, 0, 0, 0.7);
backdrop-filter: blur(12px);
}
.search-overlay .search-overlay--open .search-overlay-backdrop {
animation: search-backdrop-in 0.18s ease-out;
}
.search-overlay--closing .search-panel {
animation: search-panel-out 0.18s ease-in forwards;
}
.search-overlay--closing .search-overlay-backdrop {
animation: search-backdrop-out 0.18s ease-in forwards;
}
.search-panel-header {
display: flex;
align-items: center;
gap: 0.6rem;
}
.search-input-wrap {
flex: 1;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.8rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-surface) 96%, transparent);
}
.search-input {
flex: 1;
border: none;
outline: none;
font-size: 0.82rem;
background: transparent;
color: var(--color-text);
}
.search-input::placeholder {
color: var(--color-text-muted);
}
.search-close {
width: 2rem;
height: 2rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-bg) 90%, transparent);
color: var(--color-text-muted);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.16s ease-out, border-color 0.16s ease-out,
color 0.16s ease-out, transform 0.16s ease-out;
}
.search-close:hover {
border-color: var(--color-accent);
color: var(--color-accent);
transform: translateY(-1px);
}
.search-panel-body {
border-radius: 0.9rem;
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
background-color: color-mix(in srgb, var(--color-surface) 96%, transparent);
padding: 0.5rem 0.4rem 0.4rem;
display: flex;
flex-direction: column;
}
.search-results {
max-height: 18rem;
padding: 0.25rem 0.15rem 0.4rem;
overflow-y: auto;
}
.search-empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 1.2rem 0.5rem 1.4rem;
gap: 0.4rem;
}
.search-empty-icon {
width: 2.3rem;
height: 2.3rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-bg) 92%, transparent);
display: flex;
align-items: center;
justify-content: center;
}
.search-empty-title {
font-size: 0.86rem;
font-weight: 600;
color: var(--color-text);
}
.search-empty-subtitle {
font-size: 0.76rem;
color: var(--color-text-muted);
}
.search-footer-hints {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
border-top: 1px solid
color-mix(in srgb, var(--color-border) 75%, transparent);
padding: 0.4rem 0.5rem 0.1rem;
margin-top: 0.1rem;
}
.search-hint-key {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.68rem;
color: var(--color-text-muted);
}
.search-hint-key span:first-child,
.search-hint-key span:nth-child(2) {
padding: 0.1rem 0.3rem;
border-radius: 0.3rem;
border: 1px solid color-mix(in srgb, var(--color-border) 80%, transparent);
background-color: color-mix(in srgb, var(--color-bg) 96%, transparent);
font-size: 0.65rem;
}
.search-hint-key.search-hint-right {
margin-left: auto;
}
.search-result-item {
display: block;
padding: 0.42rem 0.55rem;
border-radius: 0.6rem;
text-decoration: none;
transition: background-color 0.12s ease-out, transform 0.12s ease-out;
}
.search-result-item:hover {
background-color: color-mix(in srgb, var(--color-surface) 92%, transparent);
transform: translateY(-1px);
}
.search-result-title {
font-size: 0.8rem;
font-weight: 500;
color: var(--color-text);
}
.search-result-meta {
margin-top: 0.15rem;
font-size: 0.7rem;
color: var(--color-text-muted);
}
.search-footer-hints {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
border-top: 1px solid
color-mix(in srgb, var(--color-border) 75%, transparent);
padding: 0.4rem 0.5rem 0.1rem;
margin-top: 0.1rem;
}
.search-hint-key {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.68rem;
color: var(--color-text-muted);
}
.search-hint-key span:first-child,
.search-hint-key span:nth-child(2) {
padding: 0.1rem 0.3rem;
border-radius: 0.3rem;
border: 1px solid color-mix(in srgb, var(--color-border) 80%, transparent);
background-color: color-mix(in srgb, var(--color-bg) 96%, transparent);
font-size: 0.65rem;
}
.search-hint-key.search-hint-right {
margin-left: auto;
}
.search-result-item {
display: block;
padding: 0.42rem 0.55rem;
border-radius: 0.6rem;
text-decoration: none;
transition: background-color 0.12s ease-out, transform 0.12s ease-out;
}
.search-result-item:hover {
background-color: color-mix(in srgb, var(--color-surface) 92%, transparent);
transform: translateY(-1px);
}
.search-result-title {
font-size: 0.8rem;
font-weight: 500;
color: var(--color-text);
}
.search-result-meta {
margin-top: 0.15rem;
font-size: 0.7rem;
color: var(--color-text-muted);
}

View File

@@ -0,0 +1,111 @@
/* ==========================================================================
TECH MARQUEE
Technology carousel/strip component
========================================================================== */
.tech-carousel {
display: flex;
gap: 0.5rem;
padding: 0.3rem 0.1rem 0.1rem;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.tech-carousel::-webkit-scrollbar {
height: 4px;
}
.tech-carousel::-webkit-scrollbar-thumb {
border-radius: 999px;
background-color: rgba(255, 255, 255, 0.12);
}
.tech-pill {
scroll-snap-align: start;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.7rem;
border-radius: 999px;
border: 1px solid var(--color-border);
background-color: color-mix(in srgb, var(--color-surface) 92%, transparent);
box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
white-space: nowrap;
}
.badge-available {
@apply inline-flex items-center gap-1.5;
padding: 0.25rem 0.7rem;
border-radius: 999px;
background-color: var(--color-accent);
color: white;
font-size: 0.7rem;
font-weight: 600;
box-shadow: 0 0 0.4rem rgba(0, 0, 0, 0.4);
}
/* --- Tech marquee --- */
.tech-icon {
font-size: 1.5rem;
}
.tech-strip {
position: relative;
overflow: hidden;
padding-block: 0.4rem;
}
.tech-strip-track {
@apply py-2;
display: inline-flex;
align-items: center;
white-space: nowrap;
animation-name: tech-marquee;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-play-state: running;
}
.tech-strip-track {
gap: 1.5rem;
}
.tech-strip--wide .tech-strip-track--primary {
animation-duration: 40s;
}
.tech-strip--wide .tech-strip-track--secondary {
animation-duration: 40s;
animation-direction: reverse;
}
.tech-strip--compact .tech-strip-track--primary {
animation-duration: 40s;
}
.tech-strip--compact .tech-strip-track--secondary {
animation-duration: 48s;
animation-direction: reverse;
}
.tech-strip--compact .tech-strip-track {
gap: 1.1rem;
}
/* pause both rows on hover */
.tech-strip:hover .tech-strip-track {
animation-play-state: paused;
}
.tech-strip-item {
display: inline-flex;
align-items: center;
gap: 0.4rem;
opacity: 0.88;
transition: opacity 0.15s ease-out, transform 0.15s ease-out;
}
.tech-strip-item:hover {
opacity: 1;
transform: translateY(-1px);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,211 @@
/* ==========================================================================
TABLE OF CONTENTS
TOC sidebar, active link tracking, responsive behavior
========================================================================== */
.article-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
position: relative;
min-width: 0;
}
@media (min-width: 1024px) {
.article-layout {
grid-template-columns: 260px 1fr;
gap: 3rem;
}
.article-toc {
order: 1;
}
.article-main {
order: 2;
}
}
.article-main {
min-width: 0;
overflow-x: hidden;
}
.article-toc {
width: 100%;
}
.toc-wrapper {
position: sticky;
top: 2rem;
background: color-mix(in srgb, var(--color-surface) 50%, transparent);
border: 1px solid var(--color-border);
border-radius: 0.85rem;
padding: 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
transition: all 0.2s ease-out;
max-height: calc(100vh - 24rem);
overflow: hidden;
display: flex;
flex-direction: column;
}
.toc-wrapper:hover {
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
}
.toc-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
}
.toc-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
font-weight: 650;
color: var(--color-text);
margin: 0;
}
.toc-title i {
color: var(--color-accent);
font-size: 0.75rem;
}
.toc-toggle {
display: flex;
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-text-muted);
width: 1.75rem;
height: 1.75rem;
border-radius: 0.4rem;
cursor: pointer;
transition: all 0.15s ease-out;
align-items: center;
justify-content: center;
}
.toc-toggle:hover {
color: var(--color-accent);
border-color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
}
.toc-toggle i {
font-size: 0.7rem;
transition: transform 0.2s ease-out;
}
.toc-wrapper.collapsed .toc-toggle i {
transform: rotate(-90deg);
}
.toc-wrapper.collapsed .toc-nav {
max-height: 0;
opacity: 0;
overflow: hidden;
}
@media (max-width: 1023px) {
.toc-wrapper {
position: relative;
top: 0;
}
}
.toc-nav {
font-size: 0.8rem;
line-height: 1.6;
transition: all 0.3s ease-out;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
min-height: 0;
}
/* Custom scrollbar for TOC */
.toc-nav::-webkit-scrollbar {
width: 6px;
}
.toc-nav::-webkit-scrollbar-track {
background: color-mix(in srgb, var(--color-bg) 30%, transparent);
border-radius: 3px;
}
.toc-nav::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--color-border) 70%, transparent);
border-radius: 3px;
}
.toc-nav::-webkit-scrollbar-thumb:hover {
background: var(--color-accent);
}
.toc-nav > ul {
list-style: none;
padding-left: 0;
margin: 0;
}
.toc-nav ul {
list-style: none;
margin: 0;
}
.toc-nav ul ul {
padding-left: 1rem;
margin-top: 0.25rem;
border-left: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
}
.toc-nav li {
margin: 0.35rem 0;
padding-left: 0;
}
.toc-nav li:hover {
background: none;
}
.toc-nav a {
display: block;
padding: 0.35rem 0.5rem;
color: var(--color-text-muted);
text-decoration: none;
border-left: 2px solid transparent;
border-radius: 0.35rem;
transition: all 0.15s ease-out;
border-bottom: none;
}
.toc-nav a:hover {
color: var(--color-text);
background: color-mix(in srgb, var(--color-surface) 40%, transparent);
border-left-color: var(--color-accent);
transform: translateX(3px);
}
.toc-nav a.active {
color: var(--color-accent);
background: color-mix(in srgb, var(--color-accent) 12%, transparent);
border-left-color: var(--color-accent);
font-weight: 600;
}
/* Mobile: TOC appears at top */
@media (max-width: 1023px) {
.article-toc {
order: -1;
margin-bottom: 1.5rem;
}
}

View File

@@ -0,0 +1,28 @@
/* ==========================================================================
MINIMAL BLACK THEME - MAIN STYLESHEET
A minimal, dark-mode first Hugo theme
========================================================================== */
/* Base Styles & Tailwind */
@import "base.css";
/* Utility Classes */
@import "utilities.css";
/* Components */
@import "components/dock.css";
@import "components/cards.css";
@import "components/navigation.css";
@import "components/search.css";
@import "components/tech-marquee.css";
/* Content */
@import "content/markdown.css";
@import "content/toc.css";
/* Pages */
@import "pages/about.css";
@import "pages/about-alternative.css";
/* Responsive Styles */
@import "responsive.css";

View File

@@ -0,0 +1,363 @@
/* ==========================================================================
ABOUT ALTERNATIVE PAGE STYLES
Alternative about page with sidebar profile card
========================================================================== */
.page-int, .about-alt-page {
max-width: 1200px;
margin-inline: auto;
}
.about-alt-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
padding: 2rem 0;
}
@media (min-width: 1024px) {
.about-alt-layout {
grid-template-columns: 320px 1fr;
gap: 3rem;
}
}
/* Profile Card - Left Sidebar */
.about-alt-sidebar {
position: relative;
}
.about-alt-profile-card {
background: linear-gradient(
135deg,
color-mix(in srgb, var(--color-surface) 70%, transparent),
color-mix(in srgb, var(--color-surface) 50%, transparent)
);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
border-radius: 1.5rem;
padding: 2rem;
text-align: center;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
position: sticky;
top: 2rem;
}
.about-alt-avatar,
.about-alt-avatar-placeholder {
width: 120px;
height: 120px;
border-radius: 50%;
margin: 0 auto 1.5rem;
overflow: hidden;
border: 4px solid var(--color-accent);
box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-accent) 15%, transparent);
transition: all 0.3s ease-out;
}
.about-alt-avatar:hover,
.about-alt-avatar-placeholder:hover {
transform: scale(1.05);
box-shadow: 0 0 0 12px color-mix(in srgb, var(--color-accent) 20%, transparent);
}
.about-alt-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.about-alt-avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--color-accent) 30%, transparent),
color-mix(in srgb, var(--color-accent) 15%, transparent)
);
}
.about-alt-avatar-placeholder i {
font-size: 3rem;
color: var(--color-accent);
}
.about-alt-name {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 0.5rem;
}
.about-alt-role {
font-size: 0.9rem;
color: var(--color-text-muted);
line-height: 1.5;
margin-bottom: 1rem;
}
.about-alt-meta {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-size: 0.85rem;
color: var(--color-text-muted);
margin-bottom: 1.5rem;
}
.about-alt-meta i {
color: var(--color-accent);
}
/* Stats Grid */
.about-alt-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin: 2rem 0;
padding: 1.5rem 0;
border-top: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
}
.about-alt-stat {
text-align: center;
}
.about-alt-stat-value {
font-size: 1.75rem;
font-weight: 700;
color: var(--color-accent);
margin-bottom: 0.25rem;
}
.about-alt-stat-label {
font-size: 0.7rem;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* Social Icons */
.about-alt-social {
display: flex;
justify-content: center;
gap: 0.75rem;
margin-top: 1.5rem;
}
.about-alt-social-icon {
display: flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
background: color-mix(in srgb, var(--color-bg) 50%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
color: var(--color-text-muted);
transition: all 0.2s ease-out;
}
.about-alt-social-icon:hover {
background: var(--color-accent);
border-color: var(--color-accent);
color: white;
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
}
.about-alt-social-icon i {
font-size: 1rem;
}
/* Content Area */
.about-alt-content {
display: flex;
flex-direction: column;
gap: 2rem;
}
.about-alt-section {
background: color-mix(in srgb, var(--color-surface) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
border-radius: 1.25rem;
padding: 2rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.about-alt-section-title {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.5rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid color-mix(in srgb, var(--color-accent) 20%, transparent);
}
.about-alt-section-title i {
color: var(--color-accent);
font-size: 1.25rem;
}
/* Experience Cards Grid */
.about-alt-experience-grid {
display: grid;
gap: 1.5rem;
}
.about-alt-experience-card {
background: color-mix(in srgb, var(--color-bg) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
border-left: 4px solid var(--color-accent);
border-radius: 0.85rem;
padding: 1.5rem;
transition: all 0.3s ease-out;
position: relative;
overflow: hidden;
}
.about-alt-experience-card::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 100px;
height: 100px;
background: radial-gradient(
circle at center,
color-mix(in srgb, var(--color-accent) 10%, transparent),
transparent 70%
);
opacity: 0;
transition: opacity 0.3s ease-out;
}
.about-alt-experience-card:hover {
border-left-width: 6px;
transform: translateX(6px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.about-alt-experience-card:hover::before {
opacity: 1;
}
.about-alt-experience-card p:first-child {
margin-top: 0;
}
.about-alt-experience-card p:last-child {
margin-bottom: 0;
}
.about-alt-experience-card strong {
font-size: 1.1rem;
color: var(--color-text);
display: block;
margin-bottom: 0.25rem;
}
.about-alt-experience-card em {
font-size: 0.85rem;
color: var(--color-text-muted);
font-style: normal;
display: block;
margin-bottom: 0.75rem;
}
/* Skills Badge Cloud */
.about-alt-skills {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.about-alt-skill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: color-mix(in srgb, var(--color-bg) 50%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
border-radius: 999px;
font-size: 0.85rem;
font-weight: 500;
color: var(--color-text);
transition: all 0.2s ease-out;
cursor: default;
}
.about-alt-skill i {
font-size: 1.1em;
opacity: 0.9;
}
.about-alt-skill:hover {
background: var(--color-accent);
border-color: var(--color-accent);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.about-alt-skill:hover i {
opacity: 1;
}
/* Responsive */
@media (max-width: 1023px) {
.about-alt-profile-card {
position: relative;
top: 0;
}
.about-alt-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.about-alt-profile-card {
padding: 1.5rem;
}
.about-alt-avatar,
.about-alt-avatar-placeholder {
width: 100px;
height: 100px;
}
.about-alt-name {
font-size: 1.5rem;
}
.about-alt-stats {
gap: 0.75rem;
}
.about-alt-stat-value {
font-size: 1.5rem;
}
.about-alt-section {
padding: 1.5rem;
}
.about-alt-section-title {
font-size: 1.25rem;
}
.about-alt-experience-card {
padding: 1.25rem;
}
}
/* ==================== */
/* TABLE OF CONTENTS */
/* ==================== */

View File

@@ -0,0 +1,380 @@
/* ==========================================================================
ABOUT PAGE STYLES
Standard about page with timeline
========================================================================== */
.about-page {
max-width: 900px;
margin-inline: auto;
}
.about-hero {
text-align: center;
padding: 3rem 0 4rem;
position: relative;
}
.about-hero::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(
90deg,
transparent,
var(--color-accent),
transparent
);
border-radius: 999px;
}
.about-hero-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.about-avatar,
.about-avatar-placeholder {
width: 140px;
height: 140px;
border-radius: 50%;
overflow: hidden;
border: 3px solid var(--color-accent);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2),
0 0 0 8px color-mix(in srgb, var(--color-accent) 15%, transparent);
transition: all 0.3s ease-out;
}
.about-avatar:hover,
.about-avatar-placeholder:hover {
transform: translateY(-4px) scale(1.02);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3),
0 0 0 12px color-mix(in srgb, var(--color-accent) 20%, transparent);
}
.about-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.about-avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--color-accent) 20%, transparent),
color-mix(in srgb, var(--color-accent) 10%, transparent)
);
backdrop-filter: blur(10px);
}
.about-avatar-placeholder i {
font-size: 4rem;
color: var(--color-accent);
}
.about-title {
font-size: 2.5rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-text);
margin: 0;
line-height: 1.2;
}
.about-subtitle {
font-size: 1.1rem;
color: var(--color-text-muted);
max-width: 600px;
line-height: 1.6;
margin: 0;
}
.about-content {
margin-bottom: 4rem;
}
.about-content .card {
background: color-mix(in srgb, var(--color-surface) 60%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
padding: 2rem;
}
.about-content .markdown-body h3 {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 2.5rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid color-mix(in srgb, var(--color-accent) 30%, transparent);
}
.about-content .markdown-body h3::before {
content: '';
display: inline-block;
width: 4px;
height: 1.5rem;
background: var(--color-accent);
border-radius: 999px;
}
/* Timeline */
.timeline {
position: relative;
padding: 2rem 0 1rem 0;
margin-top: 2rem;
}
.timeline::before {
content: '';
position: absolute;
left: 20px;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(
to bottom,
transparent,
var(--color-accent) 10%,
var(--color-accent) 90%,
transparent
);
}
.timeline-item {
position: relative;
padding-left: 60px;
margin-bottom: 3rem;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-marker {
position: absolute;
left: 0;
top: 8px;
width: 42px;
height: 42px;
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.timeline-marker::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
background: var(--color-accent);
border-radius: 50%;
border: 3px solid var(--color-bg);
box-shadow:
0 0 0 4px color-mix(in srgb, var(--color-accent) 20%, transparent),
0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.3s ease-out;
}
.timeline-item:hover .timeline-marker::before {
transform: scale(1.3);
box-shadow:
0 0 0 6px color-mix(in srgb, var(--color-accent) 30%, transparent),
0 6px 16px rgba(0, 0, 0, 0.4);
}
.timeline-content {
background: color-mix(in srgb, var(--color-surface) 30%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 50%, transparent);
border-radius: 0.85rem;
padding: 1.5rem;
transition: all 0.3s ease-out;
position: relative;
overflow: hidden;
}
.timeline-content::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--color-accent);
opacity: 0;
transition: opacity 0.3s ease-out;
}
.timeline-item:hover .timeline-content {
background: color-mix(in srgb, var(--color-surface) 50%, transparent);
border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
transform: translateX(4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
.timeline-item:hover .timeline-content::before {
opacity: 1;
}
.timeline-content > p:first-child {
margin-top: 0;
}
.timeline-content > p:last-child {
margin-bottom: 0;
}
.timeline-content strong {
font-size: 1.1rem;
color: var(--color-text);
display: block;
margin-bottom: 0.25rem;
}
.timeline-content em {
font-size: 0.85rem;
color: var(--color-text-muted);
font-style: normal;
display: block;
margin-bottom: 0.75rem;
}
.timeline-content a {
color: var(--color-accent);
}
/* Remove hr from timeline */
.timeline hr {
display: none;
}
.about-social {
text-align: center;
padding: 3rem 2rem;
background: color-mix(in srgb, var(--color-surface) 40%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 60%, transparent);
border-radius: 1.25rem;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}
.about-social-title {
font-size: 1.25rem;
font-weight: 650;
color: var(--color-text);
margin-bottom: 1.5rem;
}
.about-social-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
}
.about-social-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 999px;
background: color-mix(in srgb, var(--color-bg) 60%, transparent);
border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
color: var(--color-text-muted);
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease-out;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.about-social-link:hover {
transform: translateY(-2px);
background: color-mix(in srgb, var(--color-surface) 80%, transparent);
border-color: var(--color-accent);
color: var(--color-accent);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
}
.about-social-link i {
font-size: 1.1rem;
}
/* Responsive about page */
@media (max-width: 640px) {
.about-hero {
padding: 2rem 0 3rem;
}
.about-avatar,
.about-avatar-placeholder {
width: 110px;
height: 110px;
}
.about-avatar-placeholder i {
font-size: 3rem;
}
.about-title {
font-size: 2rem;
}
.about-subtitle {
font-size: 1rem;
}
.about-social {
padding: 2rem 1.25rem;
}
.about-social-links {
flex-direction: column;
align-items: stretch;
}
.about-social-link {
justify-content: center;
}
/* Timeline responsive */
.timeline::before {
left: 12px;
}
.timeline-item {
padding-left: 40px;
}
.timeline-marker {
left: -5px;
width: 34px;
height: 34px;
}
.timeline-marker::before {
width: 12px;
height: 12px;
}
.timeline-content {
padding: 1rem;
}
.timeline-content strong {
font-size: 1rem;
}
.timeline-content em {
font-size: 0.8rem;
}
}

View File

@@ -0,0 +1,277 @@
/* ==========================================================================
RESPONSIVE STYLES
Global responsive breakpoints and mobile optimizations
========================================================================== */
/* Mobile - Small screens (up to 640px) */
@media (max-width: 640px) {
.card-pad {
padding: 1.25rem;
}
.layout-page {
padding-inline: 1rem;
padding-block: 1.5rem;
}
.layout-page-tight {
padding-inline: 1rem;
padding-block: 1rem;
}
/* Markdown body */
.markdown-body {
font-size: 0.9rem;
padding: 0;
}
/* Headings - scale down for mobile */
.markdown-body h1 {
font-size: 1.75rem;
}
.markdown-body h2 {
font-size: 1.35rem;
margin-top: 2rem;
}
.markdown-body h3 {
font-size: 1.15rem;
margin-top: 1.5rem;
}
.markdown-body h4 {
font-size: 1rem;
}
.markdown-body h5,
.markdown-body h6 {
font-size: 0.9rem;
}
/* Heading anchors - adjust for smaller screens */
.markdown-body .md-heading-anchor::before {
left: -1rem;
font-size: 0.85em;
}
/* Blockquotes */
.markdown-body blockquote,
.markdown-body .md-blockquote {
margin: 1.25rem 0;
padding: 1rem;
border-left-width: 3px;
}
.markdown-body blockquote::before,
.markdown-body .md-blockquote::before {
font-size: 2rem;
left: 0.5rem;
top: 0.25rem;
}
/* Alerts */
.markdown-body .md-alert {
padding: 0.85rem 1rem 0.85rem 2.5rem;
}
.markdown-body .md-alert::before {
left: 0.75rem;
top: 0.95rem;
font-size: 0.95rem;
}
/* Lists */
.markdown-body ul,
.markdown-body ol {
padding-left: 1.25rem;
}
/* Code blocks */
.markdown-body pre {
margin: 1rem -1rem;
border-radius: 0.5rem;
}
.markdown-body pre code {
font-size: 0.8rem;
padding: 1rem;
}
.mb-codeblock {
margin: 1.25rem -1rem;
border-radius: 0.5rem;
}
.mb-codeblock-content pre {
padding: 1rem;
}
.mb-codeblock-content pre code {
font-size: 0.8rem;
}
/* Inline code */
.markdown-body :not(pre) > code {
font-size: 0.85em;
padding: 0.1rem 0.35rem;
}
/* Tables - full bleed on mobile */
.markdown-body table {
font-size: 0.85rem;
}
.markdown-body th,
.markdown-body td {
padding: 0.5rem 0.65rem;
}
.markdown-body .table-wrap {
margin: 1.25rem -1rem;
border-radius: 0.5rem;
}
/* Images */
.markdown-body img,
.markdown-body .md-image {
margin: 1.25rem 0;
}
.markdown-body .md-image a {
border-radius: 0.5rem;
}
.markdown-body .md-image a::after {
font-size: 2rem;
}
.markdown-body .md-image figcaption {
font-size: 0.82rem;
padding: 0.4rem 0.75rem;
}
/* Page heading */
.heading-page {
font-size: 1.75rem !important;
line-height: 1.2;
}
/* Article header */
.article-main header {
padding: 0;
}
/* TOC on mobile */
.toc-wrapper {
border-radius: 0.75rem;
padding: 0.85rem;
}
.toc-nav {
font-size: 0.85rem;
}
}
/* Tablet - Medium screens (641px to 1023px) */
@media (min-width: 641px) and (max-width: 1023px) {
.article-layout {
display: flex;
flex-wrap: wrap;
}
.article-main {
max-width: -webkit-fill-available;
}
.layout-page {
padding-inline: 1.5rem;
padding-block: 2rem;
}
.markdown-body {
font-size: 0.93rem;
}
.markdown-body h1 {
font-size: 1.85rem;
}
.markdown-body h2 {
font-size: 1.4rem;
}
.markdown-body h3 {
font-size: 1.2rem;
}
}
/* Larger screens - Adjust max-widths */
@media (min-width: 1536px) {
.article-layout {
padding-inline: 2rem;
}
}
/* Ultra-wide screens */
@media (min-width: 1920px) {
.markdown-body {
font-size: 1rem;
}
}
/* Touch device improvements */
@media (hover: none) and (pointer: coarse) {
/* Increase tap targets for touch */
.toc-nav a {
padding: 0.5rem 0.65rem;
margin: 0.15rem 0;
}
.toc-toggle {
width: 2.5rem;
height: 2.5rem;
}
.mb-action-btn {
padding: 0.5rem 0.85rem;
font-size: 0.75rem;
}
/* Remove hover effects on touch devices */
.markdown-body li:hover {
background: none;
}
.card-hover:hover {
transform: none;
box-shadow: none;
}
}
/* Landscape mobile */
@media (max-height: 500px) and (orientation: landscape) {
.toc-wrapper {
position: relative;
top: 0;
}
.layout-page {
padding-block: 1rem;
}
}
@layer base {
.prose {
color: var(--color-text);
}
.prose a {
color: var(--color-accent);
text-decoration-color: var(--color-accent);
}
.prose strong {
color: var(--color-text);
}
}

View File

@@ -0,0 +1,95 @@
/* ==========================================================================
UTILITY CLASSES
Color utilities, animations, helper classes
========================================================================== */
.bg-bg {
background-color: var(--color-bg);
}
.bg-surface {
background-color: var(--color-surface);
}
.text-text {
color: var(--color-text);
}
.text-muted {
color: var(--color-text-muted);
}
.text-accent {
color: var(--color-accent);
}
.border-border {
border-color: var(--color-border);
}
.border-accent {
border-color: var(--color-accent);
}
.bg-accent {
background-color: var(--color-accent);
}
.underline-accent {
text-decoration-color: var(--color-accent);
}
/* Simple fade-up animation */
@keyframes fade-up {
0% {
opacity: 0;
transform: translateY(8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-up {
animation: fade-up 0.45s ease-out both;
}
@keyframes search-panel-in {
0% {
opacity: 0;
transform: translate(-50%, -48%) scale(0.97);
}
100% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
@keyframes search-backdrop-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes search-panel-out {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -48%) scale(0.97);
}
}
@keyframes search-backdrop-out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@layer components { }
/* Layout helpers */

View File

@@ -0,0 +1,128 @@
+++
title = "About Me"
subtitle = "Software Engineer | Full Stack Developer | Open Source Enthusiast"
layout = "about-alternative"
+++
I'm a software engineer passionate about building elegant solutions to complex problems. Currently working on modern web applications and exploring the intersection of design, performance, and developer experience.
## What I Do
I work across the full stack with expertise in:
- **Backend Development** — Scalable APIs, microservices, database design
- **Frontend Engineering** — React, Vue, modern JavaScript frameworks
- **Cloud & DevOps** — AWS, Docker, Kubernetes, CI/CD pipelines
- **Open Source** — Contributing to projects and building developer tools
## Current Projects
Right now I'm focused on:
- Building minimal Hugo themes for personal sites
- Exploring dark-mode design patterns and accessibility
- Writing about web performance and developer experience
- Contributing to open-source projects in the web ecosystem
## About This Layout
> This page demonstrates the **alternative about layout** with a sidebar profile card.
### Key Features
This layout includes:
1. **Left Sidebar Profile Card** with:
- Avatar/profile image (or placeholder icon)
- Name and role
- Location indicator
- Customizable stats (configured in `hugo.toml`)
- Social media links
2. **Main Content Area** with:
- Introduction section
- Experience cards (from `---` separators)
- Tech stack badges (configured in `hugo.toml`)
### How to Configure
**Stats and Skills** are parametrized in your `hugo.toml`:
```toml
[params.about.alt]
# Custom stats
[[params.about.alt.stats]]
value = "5+"
label = "Years Coding"
# Tech stack with icons
[[params.about.alt.skills]]
label = "JavaScript"
icon = "devicon-javascript-plain"
```
This makes it easy to update your stats and skills without editing this page!
---
**Lead Developer** — [Modern Tech Co](https://example.com)
*2022 Present • Remote*
Leading development of cloud-native applications and mentoring engineering teams. Focus on scalable architecture, clean code practices, and continuous delivery.
---
**Senior Engineer** — Startup Ventures
*2020 2022 • San Francisco*
Built full-stack applications from scratch. Worked with React, Node.js, PostgreSQL, and AWS to deliver features serving hundreds of thousands of users.
---
**Software Engineer** — Digital Solutions
*2018 2020 • New York*
Developed enterprise applications using Java, Spring Boot, and modern frontend frameworks. Collaborated with cross-functional teams to ship quality software.
---
**Junior Developer** — Tech Academy
*2016 2018 • Boston*
Started my career building web applications and learning industry best practices. Contributed to client projects and internal tooling.
---
## Layout Comparison
| Feature | Standard About | Alternative About |
|---------|---------------|-------------------|
| Layout | Centered, single column | Sidebar + content |
| Profile Info | Top hero section | Left sidebar card |
| Stats | Not included | Configurable stats grid |
| Tech Stack | Not included | Icon badges |
| Timeline | Vertical with markers | Card-based grid |
| Best For | Traditional resumes | Modern portfolios |
Try both layouts to see which fits your style! Switch by changing `layout = "about"` or `layout = "about-alternative"` in the frontmatter.
### Responsive Design
Both layouts are fully responsive:
- **Desktop:** Sidebar + content (alternative) or centered (standard)
- **Tablet:** Stacked layout with adjusted spacing
- **Mobile:** Single column, optimized for small screens
---
## Get Started
To use this layout on your site:
1. Copy this content file structure
2. Set `layout = "about-alternative"` in frontmatter
3. Configure stats and skills in `hugo.toml`
4. Add your own content and experience
5. Optionally add an avatar image to `static/images/`
That's it! The theme handles all the styling and responsive behavior automatically.

View File

@@ -0,0 +1,111 @@
+++
title = "About Me"
subtitle = "Software Engineer | Full Stack Developer | Open Source Enthusiast"
layout = "about"
+++
I'm a passionate software engineer with expertise in building scalable web applications and developer tools. With a focus on clean code, performance optimization, and user experience, I help teams ship products that users love.
## What I Do
I specialize in full-stack development with a keen interest in:
- **Backend Engineering** — Building robust APIs and microservices with modern frameworks
- **Frontend Development** — Creating responsive, accessible user interfaces
- **DevOps & Infrastructure** — Container orchestration, CI/CD, and cloud platforms
- **Open Source** — Contributing to and maintaining open-source projects
## Timeline Demonstration
> This page demonstrates the **standard about layout** with a timeline feature. Use horizontal rules (`---`) to separate timeline entries. Each section becomes a card in the timeline visualization.
### Current Focus
Right now, I'm exploring minimal design patterns, modern web frameworks, and building tools that make developers' lives easier. I believe in writing code that is simple, maintainable, and well-documented.
---
**Senior Software Engineer** — [Tech Company](https://example.com)
*January 2022 Present • Remote*
Leading the development of cloud-native applications using modern frameworks and best practices. Mentoring junior developers and driving technical decisions for large-scale projects.
Key achievements:
- Architected microservices platform serving 1M+ users
- Reduced deployment time by 60% through CI/CD improvements
- Led migration from monolith to distributed architecture
**Technologies:** React, Node.js, PostgreSQL, Docker, Kubernetes, AWS
---
**Full Stack Developer** — Startup Inc.
*March 2020 December 2021 • San Francisco, CA*
Developed and maintained multiple web applications from concept to production. Collaborated with cross-functional teams to deliver features that increased user engagement by 40%.
- Built RESTful APIs serving 500K+ daily requests
- Implemented real-time features using WebSockets
- Optimized database queries reducing response time by 50%
**Technologies:** Python, Django, Vue.js, Redis, MySQL, GCP
---
**Software Engineer** — Digital Solutions Corp.
*June 2018 February 2020 • New York, NY*
Worked on enterprise solutions for Fortune 500 clients. Focused on backend development, database design, and system integration.
- Developed internal tools that saved 200+ hours monthly
- Integrated third-party APIs for payment processing
- Maintained legacy systems while modernizing architecture
**Technologies:** Java, Spring Boot, Oracle DB, Angular, Jenkins
---
**Junior Developer** — Code Academy
*January 2017 May 2018 • Boston, MA*
Started my professional journey building web applications and learning industry best practices. Contributed to various client projects and internal tooling.
- Developed responsive web interfaces
- Wrote automated tests achieving 90%+ coverage
- Participated in code reviews and agile ceremonies
**Technologies:** JavaScript, PHP, Laravel, MySQL, Git
---
## Education & Certifications
**Bachelor of Science in Computer Science**
University of Technology • 2013-2017
**Certifications:**
- AWS Certified Solutions Architect
- Google Cloud Professional Developer
- Certified Kubernetes Administrator (CKA)
---
## How This Layout Works
This about page uses the **timeline layout**. Here's how to customize it:
1. Set `layout = "about"` in the frontmatter
2. Write your introduction before the first `---` separator
3. Each section after `---` becomes a timeline card
4. Use markdown formatting: **bold** for job titles, *italic* for dates
5. Add links using `[text](url)` syntax
6. Timeline items appear in chronological order (newest first)
### Features Demonstrated
- ✅ Hero section with avatar and subtitle
- ✅ Timeline visualization with markers and cards
- ✅ Markdown content with formatting
- ✅ Links and emphasis
- ✅ Responsive design
- ✅ Social links in footer

View File

@@ -0,0 +1,592 @@
+++
title = "Complete Guide to Minimal Black Theme"
date = "2025-01-15"
author = "Jim Christopoulos"
tags = ["hugo", "tutorial", "guide", "theme"]
categories = ["documentation"]
description = "A comprehensive guide to all features and capabilities of the Minimal Black Hugo theme"
draft = false
+++
Welcome to the complete guide for the Minimal Black Hugo theme!
This post demonstrates all the features, components, and customization options available in the theme.
## Introduction
Minimal Black is designed for developers, designers, and everybody who want a clean, fast, and beautiful personal website.
This guide will walk you through every feature with examples you can use in your own content.
## Typography & Text Formatting
### Headings
The theme supports all six heading levels with proper hierarchy and spacing:
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
Each heading automatically gets an anchor link (hover to see the # symbol) for easy linking.
### Text Emphasis
Use standard Markdown for text formatting:
- **Bold text** for emphasis
- *Italic text* for subtle emphasis
- ***Bold and italic*** for strong emphasis
- ~~Strikethrough~~ for deleted content
- `Inline code` for code references
You can also ==highlight text== using the mark syntax (if enabled in your config).
### Links
Links are styled with subtle hover effects:
- [External link](https://example.com)
## Lists
### Unordered Lists
Simple bullet lists:
- First item
- Second item
- Nested item 1
- Nested item 2
- Deeply nested item
- Third item
### Ordered Lists
Numbered lists:
1. First step
2. Second step
1. Sub-step A
2. Sub-step B
3. Third step
### Task Lists
Interactive checkboxes:
- [x] Completed task
- [x] Another completed item
- [ ] Pending task
- [ ] Another pending item
- [x] Nested completed
- [ ] Nested pending
Task lists are great for project planning, feature tracking, or tutorial steps.
### Definition Lists
For glossary-style content:
Hugo
: A fast static site generator written in Go
Markdown
: A lightweight markup language for formatting text
Tailwind CSS
: A utility-first CSS framework
## Blockquotes
### Standard Blockquotes
Simple quotes with gradient background:
> This is a standard blockquote. It has a subtle gradient background and a colored left border for visual distinction.
> Multi-paragraph blockquotes work too.
>
> They maintain proper spacing between paragraphs while keeping the unified look.
### Nested Blockquotes
You can nest quotes:
> This is the outer quote.
>
> > This is a nested quote inside.
> >
> > > You can nest multiple levels.
### GitHub-Style Alerts
The theme supports GitHub-flavored alert callouts:
> [!NOTE]
> This is a note callout. Use it for informational content that readers should be aware of.
> [!TIP]
> This is a tip callout. Perfect for helpful suggestions and best practices.
> [!IMPORTANT]
> This is an important callout. Use it for critical information that must not be missed.
> [!WARNING]
> This is a warning callout. Great for cautionary information and potential pitfalls.
> [!CAUTION]
> This is a caution callout. Use for dangerous actions or critical warnings.
## Code Blocks
### Inline Code
Reference code inline with `backticks`.
Great for mentioning `variables`, `functions()`, or `file-names.txt`.
### Basic Code Blocks
Simple code blocks without syntax highlighting:
```
This is a plain code block.
No syntax highlighting.
Perfect for plain text or output.
```
### Syntax Highlighted Code
The theme supports syntax highlighting for dozens of languages:
**JavaScript:**
```javascript
const greet = (name) => {
console.log(`Hello, ${name}!`);
return `Welcome to the Minimal Black theme`;
};
// Async/await example
async function fetchData(url) {
try {
const response = await fetch(url);
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
}
}
```
**Python:**
```python
def fibonacci(n):
"""Calculate fibonacci number recursively."""
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# List comprehension example
squares = [x**2 for x in range(10)]
print(squares)
```
**Go:**
```go
package main
import "fmt"
func main() {
// Simple HTTP server
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
})
http.ListenAndServe(":8080", nil)
}
```
**Bash:**
```bash
#!/bin/bash
# Deploy script
hugo --minify
rsync -avz public/ user@server:/var/www/
echo "Deployment complete!"
```
**CSS:**
```css
/* Dark theme variables */
:root {
--color-bg: #000000;
--color-accent: #c084fc;
--transition: all 0.2s ease;
}
.button {
background: var(--color-accent);
transition: var(--transition);
}
```
**HTML:**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Minimal Black</title>
</head>
<body>
<h1>Welcome!</h1>
</body>
</html>
```
### Code Block Features
All code blocks include:
- **Language Label** — Shows the language in top-right corner
- **Copy Button** — Click to copy code to clipboard
- **Line Numbers** — Optional (configure in hugo.toml)
- **Syntax Highlighting** — Powered by Hugo's Chroma
## Tables
### Basic Tables
| Feature | Supported | Notes |
|---------|-----------|-------|
| Dark Mode | ✅ | True black backgrounds |
| Search | ✅ | Client-side, fast |
| TOC | ✅ | Auto-generated |
| Analytics | ✅ | Multiple providers |
### Aligned Columns
| Left Aligned | Center Aligned | Right Aligned |
|:-------------|:--------------:|--------------:|
| Left | Center | Right |
| Text | Text | 100 |
| More | Data | 250 |
### Complex Tables
| Language | Supported | Syntax Highlighting | Code Blocks |
|----------|:---------:|:-------------------:|-------------|
| JavaScript | ✅ | ✅ | Yes |
| Python | ✅ | ✅ | Yes |
| Go | ✅ | ✅ | Yes |
| Rust | ✅ | ✅ | Yes |
| TypeScript | ✅ | ✅ | Yes |
Tables are responsive and scroll horizontally on small screens.
## Images
### Basic Image
![Example image](https://images.pexels.com/photos/546819/pexels-photo-546819.jpeg)
### Image with Caption
Images automatically get a lightbox overlay on hover (magnifying glass icon appears).
### Multiple Images
![Image 1](https://images.pexels.com/photos/374560/pexels-photo-374560.jpeg)
![Image 2](https://images.pexels.com/photos/1972464/pexels-photo-1972464.jpeg)
> [!NOTE]
> *Stock Images obtained from pexels.com*
Images are responsive and scale to fit the content width.
## Gallery Shortcode
Use the gallery shortcode for image collections:
{{< gallery >}}
![Gallery 1](https://images.pexels.com/photos/1972464/pexels-photo-1972464.jpeg)
![Gallery 2](https://images.pexels.com/photos/374560/pexels-photo-374560.jpeg)
![Gallery 3](https://images.pexels.com/photos/546819/pexels-photo-546819.jpeg)
{{< /gallery >}}
The gallery includes:
- Lightbox functionality
- Click to view full size
- Navigate between images
- Responsive grid layout
## Mermaid Diagrams
### Flowchart
```mermaid
graph TD
A[Start] --> B{Is it dark mode?}
B -->|Yes| C[Load dark theme]
B -->|No| D[Load light theme]
C --> E[Render page]
D --> E
E --> F[User sees beautiful site]
```
### Sequence Diagram
```mermaid
sequenceDiagram
participant User
participant Browser
participant Hugo
participant Theme
User->>Browser: Visit site
Browser->>Hugo: Request page
Hugo->>Theme: Render template
Theme->>Hugo: Return HTML
Hugo->>Browser: Serve page
Browser->>User: Display site
```
### Class Diagram
```mermaid
classDiagram
class Theme {
+String name
+String version
+render()
+configure()
}
class Config {
+Object params
+validate()
}
class Content {
+String title
+Date date
+render()
}
Theme --> Config
Theme --> Content
```
## Horizontal Rules
Separate sections with horizontal rules:
---
Like this one above. They create clear visual breaks in content.
## Footnotes
You can add footnotes[^1] to your content. They appear at the bottom of the page[^2].
[^1]: This is a footnote. It provides additional context without interrupting the flow.
[^2]: Footnotes are automatically numbered and linked.
## Shortcodes
### Alert Shortcode
Use the alert shortcode for callouts:
{{< alert type="note" >}}
This is a custom alert using the shortcode syntax. It's an alternative to GitHub-style alerts.
{{< /alert >}}
{{< alert type="warning" >}}
Warning alerts grab attention for important notices.
{{< /alert >}}
## Table of Contents
This page demonstrates the automatic table of contents:
- **Desktop:** TOC appears in left sidebar
- **Tablet:** TOC is hidden for more reading space
- **Mobile:** TOC is completely hidden
The TOC automatically:
- Tracks your scroll position
- Highlights the current section
- Links to all headings (H2-H4)
- Stays visible while scrolling (sticky)
## Dark & Light Mode
The theme supports both dark and light modes:
### Dark Mode (Default)
- True black backgrounds (#000000)
- OLED-friendly
- Purple accents (#c084fc)
- Reduced eye strain in low light
### Light Mode
- Clean white backgrounds
- High contrast for daylight reading
- Purple accents (#a855f7)
- Print-friendly
### System Mode
- Automatically matches OS preference
- Respects user's system settings
- Seamless switching
Users can toggle between modes using the theme switcher in the navigation.
## Search Functionality
Press **Ctrl/Cmd + K** to try the search:
1. Type your query
2. Results appear instantly
3. Use arrow keys to navigate
4. Press Enter to visit page
5. ESC to close
The search:
- Is client-side (no server needed)
- Searches titles and summaries
- Highlights matching text
- Works offline
## Navigation
The theme includes:
### Header Navigation
- Logo/brand name
- Main menu links
- Theme toggle
### Footer
- Copyright notice
- Social links
- Attribution
### Floating Dock (Bottom Right)
- Quick actions
- Scroll to top
## Responsive Design
The theme is fully responsive:
### Mobile (< 640px)
- Single column layout
- Collapsible menu
- Touch-friendly buttons
- Optimized images
### Tablet (641px - 1023px)
- Two column where appropriate
- Adjusted spacing
- Tablet-optimized navigation
### Desktop (1024px+)
- Full layout with TOC sidebar
- Wider content area
- Hover effects
- Keyboard shortcuts
## Performance
The theme is optimized for performance:
- **Minimal JavaScript**
- **Optimized CSS**
- **Fast Loading**
- **SEO Optimized** — Meta tags, structured data
## Customization Examples
### Changing Accent Color
Edit `assets/css/base.css`:
```css
:root {
--color-accent: #10b981; /* Green instead of purple */
}
```
### Adding Custom Fonts
In your `hugo.toml`:
```toml
[params]
customFonts = ["https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"]
```
### Custom CSS
Create `assets/css/custom.css`:
```css
.my-custom-element {
/* Your styles */
}
```
## Best Practices
### Content Organization
1. Use clear, descriptive titles
2. Break content into sections with headings
3. Add tags and categories
4. Include descriptions in frontmatter
5. Use featured images for posts
### Performance
1. Optimize images before uploading
2. Use SVG for logos and icons
3. Minimize custom JavaScript
4. Leverage Hugo's asset pipeline
### SEO
1. Write descriptive meta descriptions
2. Use semantic HTML headings
3. Add alt text to images
4. Create an XML sitemap
5. Submit to search engines
## Conclusion
This guide covered all major features of the Minimal Black theme. You now know how to:
- ✅ Format text and create rich content
- ✅ Use code blocks with syntax highlighting
- ✅ Add images and galleries
- ✅ Create diagrams with Mermaid
- ✅ Organize content with TOC
- ✅ Customize colors and styles
- ✅ Optimize for performance
- ✅ Make content SEO-friendly
Start creating amazing content with Minimal Black!
---
**Documentation:** [Full Docs](https://gitlab.com/jimchr12/hugo-minimal-black)
**Repository:** [GitLab](https://gitlab.com/jimchr12/hugo-minimal-black)
**Issues:** [Report a Bug](https://gitlab.com/jimchr12/hugo-minimal-black/issues)

View File

@@ -0,0 +1,321 @@
+++
title = "Minimal Black Hugo Theme"
date = "2025-01-15"
description = "A minimal, dark-mode first Hugo theme"
github = "https://gitlab.com/jimchr12/hugo-minimal-black"
demo = "https://minimal-black-demo.netlify.app"
tags = ["hugo", "theme", "web-development", "open-source"]
categories = ["web"]
featured = true
+++
A comprehensive Hugo theme designed for developers, designers, and everyone in general who value simplicity, performance, and beautiful dare I say dark mode design (maybe?).
## Overview
Minimal Black is a modern Hugo theme that combines aesthetic appeal with practical functionality.
Built with Tailwind CSS and a modular architecture, it provides everything you need for a professional personal website, portfolio or blog.
### Key Features
- **True Black Dark Mode** — With purple accents
- **Responsive Design** — Perfect on mobile, tablet, and desktop
- **Search Functionality** — Fast client-side search with Ctrl/Cmd+K
- **Table of Contents** — Auto-generated
- **Syntax Highlighting** — Beautiful code blocks with copy functionality
- **Multiple Layouts** — Flexible page templates for different content types (more to come hopefully).
## Technical Architecture
### Frontend Stack
- **Hugo Extended** (v0.120.0+) — Static site generator
- **Tailwind CSS** — Utility-first CSS framework
- **Vanilla JS** — Minimal JS used - no framework
- **PostCSS** — CSS processing and optimization
### CSS Modular Structure
The CSS is organized into logical modules:
```
assets/css/
├── base.css # Theme variables & Tailwind
├── utilities.css # Helper classes
├── components/ # Reusable UI components
│ ├── dock.css
│ ├── cards.css
│ ├── navigation.css
│ ├── search.css
│ └── tech-marquee.css
├── content/ # Content-specific styles
│ ├── markdown.css
│ └── toc.css
└── pages/ # Page-specific styles
├── about.css
└── about-alternative.css
```
This modular approach makes the theme:
- Easy to maintain and extend
- Simple to customize specific components
- Clear separation of concerns
- Better developer experience
## Design Philosophy
### Minimalism First
The theme embraces minimalism without sacrificing functionality:
1. **Clean Typography** — Careful font choices and spacing
2. **Focused Content** — Remove distractions, highlight what matters
3. **Subtle Animations** — Smooth transitions that enhance UX
4. **Dark Mode Priority** — True black backgrounds
## Feature Showcase
### Search Functionality
Built-in search with keyboard shortcuts:
- Press **Ctrl/Cmd + K** to open search
- Type to filter results in real-time
- Use **Arrow keys** to navigate
- Press **Enter** to visit page
- **ESC** to close
The search is client-side, fast, and requires no external services.
### Code Blocks
Beautiful syntax highlighting with practical features:
```javascript
// Copy button on hover
function greet(name) {
console.log(`Hello, ${name}!`);
return `Welcome to Minimal Black Theme`;
}
greet("Developer");
```
Features include:
- Language labels
- Copy to clipboard button
- Line highlighting
- Collapsible / Expandable block
### GitHub-Style Alerts
Support for callout boxes:
> [!NOTE]
> This is a note alert for informational content.
> [!TIP]
> This is a tip alert with helpful suggestions.
> [!WARNING]
> This is a warning alert for important notices.
> [!IMPORTANT]
> This is an important alert for critical information.
### Table of Contents
MD content automatically gets a TOC:
- **Sticky Positioning** — Stays visible while scrolling
- **Active Tracking** — Highlights current section
- **Smooth Scrolling** — Click to jump to section
- **Responsive** — Hides on mobile, shows on desktop
You're seeing it in action on this **page**!
### Mermaid Diagrams
Native support for flowcharts and diagrams:
```mermaid
graph TD
A[User visits site] --> B{Dark mode?}
B -->|Yes| C[Load dark theme]
B -->|No| D[Load light theme]
C --> E[Render content]
D --> E
E --> F[Fast, beautiful site]
```
Perfect for technical documentation.
## Configuration
The theme is highly configurable via `hugo.toml`:
### Basic Setup
```toml
[params]
brand = "Your Name"
description = "Your site description"
[params.theme]
defaultTheme = "dark" # or "light" or "system"
```
### Hero Section
```toml
[params.hero]
badge = "Software Engineer"
title = "Hi, I'm Your Name."
role = "Building things."
summary = "Description of what you do."
location = "City, Country"
```
### Tech Stack Display
```toml
[[params.home.tech]]
label = "Python"
icon = "devicon-python-plain"
[[params.home.tech]]
label = "Docker"
icon = "devicon-docker-plain"
```
### Analytics
Support for multiple providers:
```toml
[params.analytics]
googleAnalytics = "G-XXXXXXXXXX"
# Or use Plausible, Umami, Fathom
```
## Customization Guide
### Colors
Theme colors are CSS custom properties in `assets/css/base.css`:
```css
:root {
--color-bg: #f9fafb; /* Light background */
--color-accent: #a855f7; /* Purple accent */
}
html[data-theme="dark"] {
--color-bg: #000000; /* True black */
--color-accent: #c084fc; /* Lighter purple */
}
```
### Adding Custom Styles
Create `assets/css/custom.css` in your site:
```css
/* Your custom styles */
.my-custom-class {
/* ... */
}
```
Then reference it in your config:
```toml
[params]
customCSS = ["css/custom.css"]
```
## Content Organization
### Recommended Structure
```
content/
├── _index.md # Homepage
├── about.md # About page
├── blog/
│ ├── _index.md
│ └── posts/
├── projects/
│ ├── _index.md
│ └── individual-projects/
└── pages/
└── custom-pages/
```
### Frontmatter Examples
**Blog Post:**
```yaml
+++
title = "Post Title"
date = "2025-01-15"
tags = ["hugo", "web-dev"]
categories = ["tutorials"]
draft = false
+++
```
**Project:**
```yaml
+++
title = "Project Name"
description = "Brief description"
github = "https://github.com/..." # Optional
demo = "https://demo.com" # Optional
featured = true
+++
```
## Deployment
### Netlify
1. Connect your Git repository
2. Build command: `hugo --minify`
3. Publish directory: `public`
4. Environment: `HUGO_VERSION=0.120.0`
### Vercel
1. Import repository
2. Framework: Hugo
3. Build command: `cd themes/minimal-black && npm install && cd ../.. && hugo --minify`
4. Output: `public`
## Contributing
Contributions are welcome! See [CONTRIBUTING.md](https://gitlab.com/jimchr12/hugo-minimal-black/blob/main/CONTRIBUTING.md) for guidelines.
## License
Released under the MIT License. See [LICENSE](https://gitlab.com/jimchr12/hugo-minimal-black/blob/main/LICENSE) for details.
## Acknowledgments
Built with:
- [Hugo](https://gohugo.io/) — Static site generator
- [Tailwind CSS](https://tailwindcss.com/) — CSS framework
- [Font Awesome](https://fontawesome.com/) — Icons
- [Devicon](https://devicon.dev/) — Technology icons
---
**Repository:** [GitLab](https://gitlab.com/jimchr12/hugo-minimal-black)
**Demo:** [Live Preview](https://minimal-black-demo.netlify.app)
**Documentation:** [Full Docs](https://gitlab.com/jimchr12/hugo-minimal-black#readme)

View File

@@ -0,0 +1,362 @@
baseURL = 'https://minimal-black-demo.netlify.app'
languageCode = 'en-us'
title = 'Minimal Black Theme'
theme = "minimal-black"
# Enable search index generation and web app manifest
[outputs]
home = ["HTML", "RSS", "JSON", "WebAppManifest"]
[outputFormats.WebAppManifest]
mediaType = "application/manifest+json"
rel = "manifest"
baseName = "manifest"
isPlainText = true
notAlternative = true
[params]
brand = "Hugo Minimal Black"
description = "A minimal, dark-mode first personal site, or whatever else you want it to be about"
# Favicon - Place your favicon files in /static/ directory
# Supported formats: .ico, .png, .svg
favicon = "icons/favicon.svg"
appleTouchIcon = "apple-touch-icon.png"
# Logo (optional)
logo = "images/logo.png"
# Project and blog intro text
projectsIntro = "Selected projects to display list." # Can be empty
blogIntro = "" # Can be empty
# Web App Manifest Configuration (PWA support)
[params.manifest]
themeColor = "#a855f7"
backgroundColor = "#000000"
categories = ["blog", "portfolio", "developer"]
# Custom icon configuration (optional)
# If not specified, defaults to /icons/android-chrome-*.png
# [[params.manifest.icons]]
# src = "/icons/android-chrome-192x192.png"
# sizes = "192x192"
# type = "image/png"
# purpose = "any maskable"
# Theme Configuration
[params.theme]
defaultTheme = "dark" # Options: "light", "dark", "system"
# Home Page Configuration
[params.home]
sections = ["hero", "now", "tech-marquee", "projects", "posts"]
showNowSection = true
showFeaturedProjects = true
showLatestPosts = true
featuredProjectsLimit = 3
latestPostsLimit = 3
projectsTitle = "Selected Work"
projectsSubtitle = ""
blogTitle = "Latest Writing"
blogSubtitle = ""
techMarqueeLabel = "Experienced In"
# Technology Stack Display
[[params.home.tech]]
label = "Python"
icon = "devicon-python-plain"
[[params.home.tech]]
label = "Java"
icon = "devicon-java-plain"
[[params.home.tech]]
label = "Spring"
icon = "devicon-spring-plain"
[[params.home.tech]]
label = "Vaadin"
icon = "devicon-vaadin-plain"
[[params.home.tech]]
label = "Debian"
icon = "devicon-debian-plain"
[[params.home.tech]]
label = "GitLab"
icon = "devicon-gitlab-plain"
[[params.home.tech]]
label = "Docker"
icon = "devicon-docker-plain"
[[params.home.techReverse]]
label = "PostgreSQL"
icon = "devicon-postgresql-plain"
[[params.home.techReverse]]
label = "MongoDB"
icon = "devicon-mongodb-plain"
[[params.home.techReverse]]
label = "Git"
icon = "devicon-git-plain"
[[params.home.techReverse]]
label = "Hugo"
icon = "devicon-hugo-plain"
[[params.home.techReverse]]
label = "Jenkins"
icon = "devicon-jenkins-plain"
[[params.home.techReverse]]
label = "JetBrains"
icon = "devicon-jetbrains-plain"
[[params.home.techReverse]]
label = "Json"
icon = "devicon-json-plain"
[[params.home.techReverse]]
label = "Linux"
icon = "devicon-linux-plain"
[[params.home.techReverse]]
label = "Liquibase"
icon = "devicon-liquibase-plain"
[[params.home.techReverse]]
label = "Markdown"
icon = "devicon-markdown-original"
[[params.home.techReverse]]
label = "YAML"
icon = "devicon-yaml-plain"
[[params.home.techReverse]]
label = "Wordpress"
icon = "devicon-wordpress-plain"
[[params.home.techReverse]]
label = "WooCommerce"
icon = "devicon-woocommerce-plain"
[[params.home.techReverse]]
label = "Traefik"
icon = "devicon-traefikproxy-plain"
[[params.home.techReverse]]
label = "pfSense"
icon = "devicon-pfsense-plain"
# About Page Alternative Layout Configuration - Optional
[params.about.alt]
# Stats displayed in the profile card sidebar
[[params.about.alt.stats]]
value = "5+"
label = "Years Coding"
[[params.about.alt.stats]]
value = "20+"
label = "Projects"
[[params.about.alt.stats]]
value = "∞"
label = "Hours Spent"
# Skills/Tech Stack badges with icons
[[params.about.alt.skills]]
label = "JavaScript"
icon = "devicon-javascript-plain"
[[params.about.alt.skills]]
label = "Python"
icon = "devicon-python-plain"
[[params.about.alt.skills]]
label = "React"
icon = "devicon-react-original"
[[params.about.alt.skills]]
label = "Docker"
icon = "devicon-docker-plain"
[[params.about.alt.skills]]
label = "PostgreSQL"
icon = "devicon-postgresql-plain"
[[params.about.alt.skills]]
label = "AWS"
icon = "devicon-amazonwebservices-plain"
# Hero Section Configuration
[params.hero]
badge = "Software Engineer"
title = "Hi, I'm Your Name or your interesting title."
role = "Subtitle for title with role perspective"
summary = "You can write your summary to be displayed here."
# avatar = "images/avatar.jpg" # Optional: 400x400px recommended
location = "City, Country"
focus = "Currently focused on Hugo themes & developer experience."
available = true
availableLabel = "Available for work"
nowLabel = "Quick Facts"
nowIntro = "Right now I'm mainly:"
now = [
"Building minimal Hugo themes",
"Exploring dark-mode design patterns",
"Writing about web performance"
]
[params.hero.primary]
label = "View Projects"
href = "/projects/"
[params.hero.secondary]
label = "Read the Blog"
href = "/blog/"
# Icon Libraries
[params.icons]
useFontAwesome = true
useDevicon = true
# Social Links
[[params.social]]
label = "GitLab"
url = "https://gitlab.com/jimchr12"
icon = "fa-brands fa-gitlab"
[[params.social]]
label = "LinkedIn"
url = "https://www.linkedin.com/in/jimchristopoulos-542512221/"
icon = "fa-brands fa-linkedin-in"
[[params.social]]
label = "Email"
url = "mailto:you@example.com"
icon = "fa-regular fa-envelope"
# Analytics Configuration
[params.analytics]
# Google Analytics (GA4)
# googleAnalytics = "G-XXXXXXXXXX"
# Plausible Analytics (privacy-friendly)
# [params.analytics.plausible]
# enabled = true
# domain = "yourdomain.com"
# scriptUrl = "https://plausible.io/js/script.js"
# Umami Analytics (self-hosted option)
# [params.analytics.umami]
# enabled = true
# scriptUrl = "https://analytics.yourdomain.com/script.js"
# websiteId = "your-website-id"
# Fathom Analytics
# [params.analytics.fathom]
# enabled = true
# scriptUrl = "https://cdn.usefathom.com/script.js"
# siteId = "YOUR-SITE-ID"
# Navigation Menu
[menu]
[[menu.main]]
name = "Home"
pageRef = "/"
url = "/"
weight = 1
identifier = "home"
[menu.main.params]
icon = "fa-solid fa-house"
[[menu.main]]
name = "About"
pageRef = "about"
url = "/about/"
weight = 2
identifier = "about"
[menu.main.params]
icon = "fa-regular fa-user"
[[menu.main]]
name = "About Alt"
pageRef = "about-alternative"
url = "/about-alternative/"
weight = 2
identifier = "about-alternative"
[menu.main.params]
icon = "fa-solid fa-user"
[[menu.main]]
name = "Projects"
pageRef = "projects"
url = "/projects/"
weight = 3
identifier = "projects"
[menu.main.params]
icon = "fa-regular fa-folder-open"
[[menu.main]]
name = "Blog"
pageRef = "blog"
url = "/blog/"
weight = 4
identifier = "blog"
[menu.main.params]
icon = "fa-regular fa-note-sticky"
# Markup Configuration
[markup]
# Table of Contents
[markup.tableOfContents]
startLevel = 2
endLevel = 4
# Goldmark Renderer (Markdown)
[markup.goldmark.renderer]
unsafe = true
[markup.goldmark.parser]
[markup.goldmark.parser.attribute]
block = true
[markup.goldmark.extensions]
typographer = true
linkify = true
table = true
strikethrough = true
taskList = true
definitionList = true
footnote = true
[markup.goldmark.extensions.extras.delete]
enable = true
[markup.goldmark.extensions.extras.insert]
enable = true
[markup.goldmark.extensions.extras.mark]
enable = true
# Syntax Highlighting
[markup.highlight]
codeFences = true
guessSyntax = true
lineNos = false
lineNumbersInTable = false
noClasses = false
style = "monokai"
tabWidth = 2
# Taxonomies
[taxonomies]
tag = "tags"
category = "categories"
# Privacy Configuration
[privacy]
[privacy.youtube]
privacyEnhanced = true

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

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 }}

View File

@@ -0,0 +1,29 @@
[build]
publish = "public"
command = "mkdir -p hugo-minimal-black && rsync -av --exclude='hugo-minimal-black' --exclude='.git' --exclude='public' . ./hugo-minimal-black/ && cd hugo-minimal-black/exampleSite && hugo --gc --minify --themesDir ../.. --theme hugo-minimal-black && mv public ../../public"
[build.environment]
HUGO_VERSION = "0.152.2"
HUGO_ENV = "production"
HUGO_ENABLEGITINFO = "true"
[context.production.environment]
HUGO_VERSION = "0.152.2"
HUGO_ENV = "production"
[context.deploy-preview]
command = "mkdir -p hugo-minimal-black && rsync -av --exclude='hugo-minimal-black' --exclude='.git' --exclude='public' . ./hugo-minimal-black/ && cd hugo-minimal-black/exampleSite && hugo --gc --minify --buildFuture --themesDir ../.. --theme hugo-minimal-black && mv public ../../public"
[context.deploy-preview.environment]
HUGO_VERSION = "0.152.2"
[context.branch-deploy]
command = "mkdir -p hugo-minimal-black && rsync -av --exclude='hugo-minimal-black' --exclude='.git' --exclude='public' . ./hugo-minimal-black/ && cd hugo-minimal-black/exampleSite && hugo --themesDir ../.. --theme hugo-minimal-black && mv public ../../public"
[context.branch-deploy.environment]
HUGO_VERSION = "0.152.2"
[[redirects]]
from = "/*"
to = "/404.html"
status = 404

View File

@@ -0,0 +1,19 @@
{
"name": "minimal-black",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Jim Christopoulos",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"autoprefixer": "^10.4.22",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18"
}
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,21 @@
{
"name": "Minimal Black",
"short_name": "M-BLK",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,16 @@
document.addEventListener("DOMContentLoaded", function () {
if (typeof window.$ === "undefined" && typeof window.JustifiedGallery === "undefined") {
// using vanilla JustifiedGallery from CDN, globally exposed
}
var roots = document.querySelectorAll("[data-jg]");
if (!roots.length || typeof window.JustifiedGallery === "undefined") return;
roots.forEach(function (el) {
new window.JustifiedGallery(el, {
rowHeight: 260,
lastRow: "center",
margin: 16,
});
});
});

View File

@@ -0,0 +1,11 @@
document.addEventListener("DOMContentLoaded", function () {
if (typeof GLightbox === "undefined") return;
GLightbox({
selector: ".glightbox",
loop: true,
touchNavigation: true,
zoomable: true,
draggable: true,
});
});

View File

@@ -0,0 +1,137 @@
(function () {
var STORAGE_KEY = "theme";
/* ---------- Theme ---------- */
function getStoredTheme() {
try {
return localStorage.getItem(STORAGE_KEY);
} catch (e) {
return null;
}
}
function storeTheme(theme) {
try {
localStorage.setItem(STORAGE_KEY, theme);
} catch (e) {}
}
function getSystemTheme() {
return window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
var isDark = theme === "dark";
var lightIcons = document.querySelectorAll("[data-theme-icon-light]");
var darkIcons = document.querySelectorAll("[data-theme-icon-dark]");
lightIcons.forEach(function (el) {
el.style.display = isDark ? "" : "none";
});
darkIcons.forEach(function (el) {
el.style.display = isDark ? "none" : "";
});
}
function initThemeFromDOM() {
var attr = document.documentElement.getAttribute("data-theme");
if (attr === "dark" || attr === "light") {
applyTheme(attr);
return;
}
var stored = getStoredTheme();
applyTheme(stored || getSystemTheme());
}
function toggleTheme() {
var current =
document.documentElement.getAttribute("data-theme") || "light";
var next = current === "dark" ? "light" : "dark";
applyTheme(next);
storeTheme(next);
}
function initThemeToggle() {
var btns = document.querySelectorAll("[data-theme-toggle]");
if (!btns.length) return;
btns.forEach(function (btn) {
btn.addEventListener("click", toggleTheme);
});
}
/* ---------- Mobile nav ---------- */
function initMobileNav() {
var toggle = document.querySelector("[data-mobile-nav-toggle]");
var nav = document.querySelector("[data-mobile-nav]");
if (!toggle || !nav) return;
var open = false;
function setOpen(next) {
open = next;
nav.style.display = open ? "block" : "none";
}
toggle.addEventListener("click", function () {
setOpen(!open);
});
nav.addEventListener("click", function (e) {
if (e.target.tagName === "A") setOpen(false);
});
}
/* ---------- Dock ---------- */
function initDock() {
var dock = document.querySelector("[data-dock]");
if (!dock) return;
var toggle = dock.querySelector("[data-dock-toggle]");
var backTop = dock.querySelector('[data-dock-action="top"]');
var backBtn = dock.querySelector('[data-dock-action="back"]');
var open = false;
function setOpen(next) {
open = next;
dock.classList.toggle("dock--open", open);
}
if (toggle) {
toggle.addEventListener("click", function () {
setOpen(!open);
});
}
if (backTop) {
backTop.addEventListener("click", function (e) {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: "smooth",
});
});
}
if (backBtn) {
backBtn.addEventListener("click", function (e) {
e.preventDefault();
window.history.back();
});
}
}
/* ---------- Init ---------- */
document.addEventListener("DOMContentLoaded", function () {
initThemeFromDOM();
initThemeToggle();
initMobileNav();
initDock();
});
})();

View File

@@ -0,0 +1,253 @@
(function () {
var overlay, inputEl, resultsEl;
var indexLoaded = false;
var pages = [];
function ensureElements() {
if (!overlay) {
overlay = document.querySelector("[data-search-overlay]");
}
if (!inputEl && overlay) {
inputEl = overlay.querySelector("[data-search-input]");
}
if (!resultsEl && overlay) {
resultsEl = overlay.querySelector("[data-search-results]");
}
}
function loadIndex() {
if (indexLoaded) return;
indexLoaded = true;
fetch("/index.json")
.then(function (r) {
if (!r.ok) throw new Error("index.json not found");
return r.json();
})
.then(function (data) {
pages = (data && data.pages) || [];
})
.catch(function () {
pages = [];
});
}
function openOverlay() {
ensureElements();
if (!overlay) return;
overlay.classList.remove("search-overlay");
overlay.classList.add("search-overlay--open");
loadIndex();
if (inputEl) {
setTimeout(function () {
inputEl.focus();
}, 20);
}
}
function closeOverlay() {
ensureElements();
if (!overlay) return;
if (overlay.classList.contains("search-overlay--closing")) return;
overlay.classList.add("search-overlay--closing");
setTimeout(function () {
overlay.classList.remove("search-overlay--open");
overlay.classList.remove("search-overlay--closing");
overlay.classList.add("search-overlay");
if (inputEl) inputEl.value = "";
if (resultsEl) {
resultsEl.innerHTML =
'<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>";
}
}, 180);
}
function filterPages(query) {
if (!pages.length) return [];
var q = (query || "").toLowerCase().trim();
if (!q) return [];
return pages
.filter(function (p) {
var t = (p.title || "").toLowerCase();
var s = (p.summary || "").toLowerCase();
return t.indexOf(q) !== -1 || s.indexOf(q) !== -1;
})
.slice(0, 20);
}
function highlightText(text, query) {
if (!query) return text;
var regex = new RegExp("(" + query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
return text.replace(regex, '<mark class="search-highlight">$1</mark>');
}
function getSectionIcon(section) {
var icons = {
blog: "fa-regular fa-note-sticky",
projects: "fa-regular fa-folder-open",
posts: "fa-regular fa-note-sticky",
};
return icons[section.toLowerCase()] || "fa-regular fa-file";
}
function truncateText(text, maxLength) {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength) + "...";
}
function renderResults(query) {
ensureElements();
if (!resultsEl) return;
var q = (query || "").trim();
if (!q) {
resultsEl.innerHTML =
'<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>";
return;
}
var matches = filterPages(q);
if (!matches.length) {
resultsEl.innerHTML =
'<div class="search-empty-state">' +
'<div class="search-empty-icon"><i class="fa-solid fa-circle-exclamation text-[1rem]"></i></div>' +
'<p class="search-empty-title">No results found</p>' +
'<p class="search-empty-subtitle">Try different keywords or check your spelling.</p>' +
"</div>";
return;
}
var html = matches
.map(function (p, index) {
var title = highlightText(p.title || "Untitled", q);
var section = p.section || "";
var summary = truncateText(p.summary || "", 120);
var highlightedSummary = highlightText(summary, q);
var icon = getSectionIcon(section);
var date = p.date ? new Date(p.date).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric"
}) : "";
return (
'<a href="' +
p.permalink +
'" class="search-result-item" data-result-index="' +
index +
'">' +
'<div class="search-result-header">' +
'<i class="' + icon + ' search-result-icon"></i>' +
'<div class="search-result-info">' +
'<div class="search-result-title">' +
title +
"</div>" +
'<div class="search-result-meta">' +
(section ? '<span class="search-result-section">' + section + "</span>" : "") +
(date ? '<span class="search-result-date">' + date + "</span>" : "") +
"</div>" +
"</div>" +
"</div>" +
(highlightedSummary ? '<div class="search-result-summary">' + highlightedSummary + "</div>" : "") +
"</a>"
);
})
.join("");
resultsEl.innerHTML = html;
// Add keyboard navigation
addKeyboardNavigation();
}
var selectedIndex = -1;
function addKeyboardNavigation() {
ensureElements();
if (!inputEl) return;
var items = resultsEl.querySelectorAll(".search-result-item");
inputEl.addEventListener("keydown", function(e) {
if (e.key === "ArrowDown") {
e.preventDefault();
selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
updateSelection(items);
} else if (e.key === "ArrowUp") {
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, -1);
updateSelection(items);
} else if (e.key === "Enter" && selectedIndex >= 0) {
e.preventDefault();
items[selectedIndex].click();
}
});
}
function updateSelection(items) {
items.forEach(function(item, index) {
if (index === selectedIndex) {
item.classList.add("search-result-item--selected");
item.scrollIntoView({ block: "nearest", behavior: "smooth" });
} else {
item.classList.remove("search-result-item--selected");
}
});
}
function initSearch() {
ensureElements();
if (!overlay) return;
// Close and ESC
overlay.querySelectorAll("[data-search-close]").forEach(function (el) {
el.addEventListener("click", closeOverlay);
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeOverlay();
});
// Typing
if (inputEl) {
inputEl.addEventListener("input", function (e) {
renderResults(e.target.value || "");
});
}
// Ctrl/Cmd + K to open
document.addEventListener("keydown", function (e) {
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") {
e.preventDefault();
openOverlay();
}
});
// Expose global API for inline onclick
window.MinimalSearch = {
open: openOverlay,
close: closeOverlay,
};
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initSearch);
} else {
initSearch();
}
})();

View File

@@ -0,0 +1,25 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class", '[data-theme="dark"]'],
content: [
"./layouts/**/*.html",
"./exampleSite/content/**/*.{md,html}",
"../../content/**/*.{md,html}",
],
theme: {
extend: {
colors: {
bg: "var(--color-bg)",
surface: "var(--color-surface)",
text: "var(--color-text)",
muted: "var(--color-text-muted)",
border: "var(--color-border)",
accent: "var(--color-accent)",
},
maxWidth: {
"3xl": "48rem",
},
},
},
plugins: [require("@tailwindcss/typography")],
};

View File

@@ -0,0 +1,29 @@
name = "Minimal Black"
license = "MIT"
licenselink = "https://gitlab.com/jimchr12/hugo-minimal-black/blob/main/LICENSE"
description = "A minimal, dark-mode first Hugo theme with true black backgrounds, purple accents, and comprehensive content styling. Features responsive design, search, TOC, and multiple page layouts."
homepage = "https://gitlab.com/jimchr12/hugo-minimal-black"
demosite = "https://minimal-black-demo.netlify.app"
tags = ["blog", "portfolio", "minimal", "dark", "tailwind", "responsive", "clean", "modern", "personal"]
features = [
"Dark Mode",
"Light Mode",
"Responsive Design",
"Search",
"Table of Contents",
"Syntax Highlighting",
"Google Analytics",
"Font Awesome Icons",
"Devicon Support",
"Multiple About Page Layouts",
"Project Portfolio",
"Blog",
"Gallery Shortcode",
"Mermaid Diagrams",
"GitHub Flavored Markdown"
]
min_version = "0.120.0"
[author]
name = "Jim Christopoulos"
homepage = "https://jimchristopoulos.com/"