summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-04-22 12:42:56 +0200
committerDanilo M. <danix@danix.xyz>2026-04-22 12:42:56 +0200
commit631547a75142326a7c71bdf123e1475217a5ad73 (patch)
treef3cfef6b3c5b42bf626fc823ddcf63b8dcf4cdbb
parent77ccbe72fad5a4870185fff374f75471c16a9043 (diff)
downloaddanixxyz-theme-631547a75142326a7c71bdf123e1475217a5ad73.tar.gz
danixxyz-theme-631547a75142326a7c71bdf123e1475217a5ad73.zip
chore: replace with extracted danix.xyz-hacker theme (danix2-hugo-theme)
-rw-r--r--.hugo_build.lock0
-rw-r--r--CONTENT_GUIDE.md938
-rw-r--r--archetypes/article.md11
-rw-r--r--archetypes/default.md7
-rw-r--r--archetypes/page.md8
-rw-r--r--assets/css/chroma-custom.css391
-rw-r--r--assets/css/components/404.css247
-rw-r--r--assets/css/components/article-hero.css225
-rw-r--r--assets/css/components/card.css254
-rw-r--r--assets/css/components/code.css138
-rw-r--r--assets/css/components/feed.css101
-rw-r--r--assets/css/components/footer.css64
-rw-r--r--assets/css/components/hamburger.css189
-rw-r--r--assets/css/components/header.css89
-rw-r--r--assets/css/components/hero.css354
-rw-r--r--assets/css/components/lightbox.css170
-rw-r--r--assets/css/components/progress-bar.css19
-rw-r--r--assets/css/components/share-sidebar.css107
-rw-r--r--assets/css/components/timeline.css336
-rw-r--r--assets/css/main.css1792
-rw-r--r--assets/css/main.min.css4495
-rw-r--r--assets/css/variables.css136
-rw-r--r--assets/js/404.js54
-rw-r--r--assets/js/article-lazy.js34
-rw-r--r--assets/js/code-copy.js79
-rw-r--r--assets/js/contact-form.js45
-rw-r--r--assets/js/copy-code.js42
-rw-r--r--assets/js/filters.js37
-rw-r--r--assets/js/form-components.js127
-rw-r--r--assets/js/fortune.js9
-rw-r--r--assets/js/glitch.js30
-rw-r--r--assets/js/hamburger.js83
-rw-r--r--assets/js/lightbox.js31
-rw-r--r--assets/js/main.js18
-rw-r--r--assets/js/matrix-rain.js179
-rw-r--r--assets/js/menu.js112
-rw-r--r--assets/js/not-found-page.js9
-rw-r--r--assets/js/photo-utils.js476
-rw-r--r--assets/js/progress-bar.js20
-rw-r--r--assets/js/reading-progress.js29
-rw-r--r--assets/js/scroll-reveal.js40
-rw-r--r--assets/js/search.js134
-rw-r--r--assets/js/share-sidebar.js51
-rw-r--r--assets/js/tag-cloud-spiral.js122
-rw-r--r--assets/js/theme-toggle.js116
-rw-r--r--assets/js/typing.js69
-rw-r--r--assets/jsconfig.json10
-rw-r--r--content/_index.md6
-rw-r--r--content/articles/_index.md6
-rw-r--r--content/is/_index.md8
-rw-r--r--content/is/here.md87
-rw-r--r--content/posts/_index.md7
-rw-r--r--content/posts/post-1.md10
-rw-r--r--content/posts/post-2.md10
-rw-r--r--content/posts/post-3/bryce-canyon.jpgbin19224 -> 0 bytes
-rw-r--r--content/posts/post-3/index.md12
-rw-r--r--hugo.toml60
-rw-r--r--i18n/en.yaml171
-rw-r--r--i18n/it.yaml173
-rw-r--r--layouts/404.en.html142
-rw-r--r--layouts/404.html98
-rw-r--r--layouts/404.it.html142
-rw-r--r--layouts/_default/_markup/render-codeblock.html23
-rw-r--r--layouts/_default/_markup/render-heading.html5
-rw-r--r--layouts/_default/baseof.html130
-rw-r--r--layouts/_default/list.html39
-rw-r--r--layouts/_default/single.html54
-rw-r--r--layouts/_partials/article-single.html56
-rw-r--r--layouts/_partials/featured-card.html23
-rw-r--r--layouts/_partials/footer.html14
-rw-r--r--layouts/_partials/head.html8
-rw-r--r--layouts/_partials/head/css.html15
-rw-r--r--layouts/_partials/head/js.html15
-rw-r--r--layouts/_partials/header.html29
-rw-r--r--layouts/_partials/hero.html41
-rw-r--r--layouts/_partials/menu.html51
-rw-r--r--layouts/_partials/photo-article.html32
-rw-r--r--layouts/_partials/post-card.html30
-rw-r--r--layouts/_partials/share-sidebar.html28
-rw-r--r--layouts/_partials/static-page.html33
-rw-r--r--layouts/_partials/terms.html23
-rw-r--r--layouts/_partials/timeline-item.html31
-rw-r--r--layouts/articles/single.html50
-rw-r--r--layouts/baseof.html23
-rw-r--r--layouts/home.html48
-rw-r--r--layouts/index.html59
-rw-r--r--layouts/index.json13
-rw-r--r--layouts/is/list.html22
-rw-r--r--layouts/page.html9
-rw-r--r--layouts/partials/article-card.html92
-rw-r--r--layouts/partials/article-header.html71
-rw-r--r--layouts/partials/article-list-item.html95
-rw-r--r--layouts/partials/article-nav.html46
-rw-r--r--layouts/partials/article-types/life.html3
-rw-r--r--layouts/partials/article-types/link.html17
-rw-r--r--layouts/partials/article-types/photo.html19
-rw-r--r--layouts/partials/article-types/quote.html15
-rw-r--r--layouts/partials/article-types/tech.html3
-rw-r--r--layouts/partials/back-to-top.html23
-rw-r--r--layouts/partials/breadcrumb-jsonld.html36
-rw-r--r--layouts/partials/breadcrumb.html16
-rw-r--r--layouts/partials/footer.html89
-rw-r--r--layouts/partials/form-components.html219
-rw-r--r--layouts/partials/hamburger-menu.html125
-rw-r--r--layouts/partials/head-meta.html57
-rw-r--r--layouts/partials/header.html94
-rw-r--r--layouts/partials/search-modal.html87
-rw-r--r--layouts/partials/sidebar.html54
-rw-r--r--layouts/partials/social-share.html123
-rw-r--r--layouts/partials/tag-cloud.html90
-rw-r--r--layouts/partials/toast-container.html13
-rw-r--r--layouts/repository/single.html77
-rw-r--r--layouts/robots.txt5
-rw-r--r--layouts/section.html40
-rw-r--r--layouts/shortcodes/actions.html26
-rw-r--r--layouts/shortcodes/contact.html65
-rw-r--r--layouts/shortcodes/div-close.html2
-rw-r--r--layouts/shortcodes/div.html4
-rw-r--r--layouts/shortcodes/dropcap.html15
-rw-r--r--layouts/shortcodes/em.html2
-rw-r--r--layouts/shortcodes/figure.html27
-rw-r--r--layouts/shortcodes/gal-img.html16
-rw-r--r--layouts/shortcodes/gallery.html13
-rw-r--r--layouts/shortcodes/gravatar.html67
-rw-r--r--layouts/shortcodes/image.html23
-rw-r--r--layouts/shortcodes/img.html84
-rw-r--r--layouts/shortcodes/quote.html32
-rw-r--r--layouts/shortcodes/strike.html2
-rw-r--r--layouts/shortcodes/svg.html5
-rw-r--r--layouts/shortcodes/video.html72
-rw-r--r--layouts/taxonomy.html7
-rw-r--r--layouts/taxonomy/list.html49
-rw-r--r--layouts/taxonomy/term.html46
-rw-r--r--layouts/term.html7
-rw-r--r--static/favicon.icobin15406 -> 0 bytes
-rw-r--r--theme.toml37
136 files changed, 10227 insertions, 5746 deletions
diff --git a/.hugo_build.lock b/.hugo_build.lock
deleted file mode 100644
index e69de29..0000000
--- a/.hugo_build.lock
+++ /dev/null
diff --git a/CONTENT_GUIDE.md b/CONTENT_GUIDE.md
deleted file mode 100644
index cdc2733..0000000
--- a/CONTENT_GUIDE.md
+++ /dev/null
@@ -1,938 +0,0 @@
-# Content Organization Guide
-
-A practical guide for managing content on your personal blog. Use this when creating, editing, or organizing articles and pages.
-
----
-
-## Table of Contents
-
-1. [Directory Structure](#directory-structure)
-2. [Content Types](#content-types)
-3. [Creating New Content](#creating-new-content)
-4. [Front Matter Reference](#front-matter-reference)
-5. [Publishing Workflow](#publishing-workflow)
-6. [Best Practices](#best-practices)
-7. [Examples](#examples)
-
----
-
-## Directory Structure
-
-```
-content/
-├── _index.md # Home page (don't modify much)
-├── articles/ # All blog content lives here
-│ ├── _index.md # Articles section landing page
-│ ├── my-first-article/
-│ │ ├── index.md # Article content
-│ │ └── featured-image.jpg # Images bundled with article
-│ ├── another-article.md
-│ └── ...
-└── is/ # Static pages (about, contact, etc.)
- ├── _index.md # About page (/)
- ├── here.md # Contact page (/is/here)
- └── ... # Other static pages
-```
-
-**Key Rules:**
-- All blog posts go in `content/articles/` — not directly in `content/`
-- Static pages (about, contact, uses, now, etc.) go in `content/is/`
-- Use folders for articles with images; use `.md` files for text-only articles
-- Folder names become URL slugs, so use lowercase and hyphens: `my-first-article/`
-
----
-
-## Navigation Menu Configuration
-
-The top navigation menu is configured in `hugo.toml`. You can easily add, remove, or reorder menu items.
-
-### How to Add Menu Items
-
-Edit your `hugo.toml` file and add a new `[[menu.main]]` block:
-
-```toml
-[[menu.main]]
- name = 'Articles'
- pageRef = '/articles'
- weight = 10
-
-[[menu.main]]
- name = 'About'
- pageRef = '/is'
- weight = 20
-
-[[menu.main]]
- name = 'Contact'
- pageRef = '/is/here'
- weight = 30
-
-# Add a new page to the menu:
-[[menu.main]]
- name = 'Uses'
- pageRef = '/is/uses'
- weight = 40
-```
-
-### Parameters
-
-- `name` — Text displayed in the navigation menu
-- `pageRef` — Path to your page (e.g., `/articles`, `/is/about`). Must match your content folder structure
-- `weight` — Controls menu order. Lower numbers appear first (leftmost), higher numbers appear last (rightmost)
-
-### External Links
-
-To link to external websites, use `url` instead of `pageRef`:
-
-```toml
-[[menu.main]]
- name = 'GitHub'
- url = 'https://github.com/danix2'
- weight = 50
-```
-
-### Creating Pages for Menu Items
-
-Before adding a menu item, create the corresponding page in `content/`:
-
-```bash
-# Create a new static page
-hugo new is/uses.md
-
-# Then add it to the menu in hugo.toml
-[[menu.main]]
- name = 'Uses'
- pageRef = '/is/uses'
- weight = 40
-```
-
----
-
-## Content Types
-
-Your blog supports five content types for articles. Each type is visually distinct in the feed with its own badge color.
-
-### **tech** (Purple #a855f7)
-Technical articles: programming tutorials, IT learnings, engineering breakdowns, code walkthroughs, tool reviews.
-
-**Examples:**
-- Understanding Kubernetes networking
-- Building a REST API with Go
-- Setting up a CI/CD pipeline
-- Debugging memory leaks
-
-### **life** (Amber #f59e0b)
-Personal reflections, philosophy, life lessons, observations about slowing down, productivity thoughts, life updates.
-
-**Examples:**
-- On slowing down in a world that won't
-- What I learned from a failed project
-- Balancing work and life
-- Reflections on burnout
-
-### **quote** (Green #00ff88)
-Meaningful quotes you find interesting, with optional context about why they matter to you.
-
-**Examples:**
-- "The obstacle is the way" — Marcus Aurelius
-- "It's about the work, not the reward" — Unknown
-- "Simplicity is the ultimate sophistication" — da Vinci
-
-### **link** (Cyan #38bdf8)
-Interesting links from the web with optional your own commentary. Can be standalone or with analysis.
-
-**Examples:**
-- [Article] The future of Rust in systems programming
-- [Video] Why Git is hard to understand
-- [Thread] Thoughts on API design principles
-
-### **photo** (Pink #ec4899)
-Photos or visual content you create and want to share. Can include captions or stories.
-
-**Examples:**
-- Photos from a recent trip
-- A project you built (with pictures)
-- Visual design work you want to showcase
-
----
-
-## Creating New Content
-
-### Quick Start: Use Hugo CLI
-
-```bash
-# Create a new article
-hugo new articles/my-article-title/index.md
-
-# Create a static page (about, contact, etc.)
-hugo new is/page-title.md
-```
-
-The CLI uses your archetypes to generate files with proper front matter.
-
-### Manual Creation
-
-If you prefer, just create `.md` files directly in the right folder. Copy the front matter structure from examples below.
-
----
-
-## Front Matter Reference
-
-Every content file starts with TOML front matter (between `+++` markers). Here's what each field does:
-
-```toml
-+++
-title = "Article Title" # Required: The page title (appears in feed and page)
-date = 2026-04-05T10:30:00Z # Required: Publication date (ISO 8601 format)
-draft = false # Optional: Set to true to hide from published site
-type = "tech" # Required for articles: tech, life, quote, link, photo
-featured = false # Optional: Set to true to pin to top of homepage
-image = "featured-image.jpg" # Optional: Image path for card thumbnail (relative to content file)
-description = "One or two..." # Optional: Short summary (2 lines max) for feed cards
-+++
-```
-
-### Field Details
-
-| Field | Required | Notes |
-|-------|----------|-------|
-| `title` | Yes | Used as page heading and in feeds. Keep under 80 chars for better layout. |
-| `date` | Yes | When the article was published. Format: `2026-04-05T10:30:00Z` (UTC). Hugo sorts by this. |
-| `draft` | No | Set to `true` to hide from published site (useful for work-in-progress). |
-| `type` | Yes (articles) | One of: `tech`, `life`, `quote`, `link`, `photo`. Not needed for static pages. |
-| `featured` | No | Set to `true` to pin one article to the top of the homepage feed. Only the first featured post shows. |
-| `image` | No | Path to a featured image (relative to the content file folder). Shows as thumbnail in card. |
-| `description` | No | Short summary for feed cards. If omitted, Hugo uses the first 120 characters of your content. Keep to 2 lines max (~150 chars). |
-
----
-
-## Publishing Workflow
-
-### Before Publishing
-
-1. **Write your content** — Use the body section after `+++` for markdown
-2. **Add front matter** — Fill in title, date, type, description, image (if applicable)
-3. **Set draft = false** — Only non-draft content appears on the live site
-4. **Preview locally** — Run `hugo server` to see how it looks
-
-### Publishing
-
-```bash
-# Start local preview
-hugo server
-
-# Open http://localhost:1313 in your browser
-# Check homepage, articles page, filters, detail pages
-
-# When satisfied:
-git add content/
-git commit -m "content: add [title] article"
-git push origin master
-```
-
-### Important Notes
-
-- **Draft status**: Set `draft = true` while writing. Change to `draft = false` when ready to publish.
-- **Featured**: Only set one article to `featured = true`. It will appear first on the homepage, even if it's not the newest.
-- **Dates in the future**: Articles with future dates are treated as drafts (won't appear until that date passes).
-
----
-
-## Best Practices
-
-### Article Length & Structure
-
-- **Tech articles**: 800–2000 words. Include code examples, explanations, and takeaways.
-- **Life reflections**: 300–1000 words. Personal, conversational tone is best.
-- **Quotes**: 50–200 words. Include the quote, attribution, and 1–2 sentences on why it resonates.
-- **Links**: 50–500 words. Headline, link, and 1–3 sentences of context or summary.
-- **Photos**: 100–500 words. Caption, story, or context about the image.
-
-### Writing Tips
-
-1. **Titles should be specific** — "Understanding X" is better than "Learning about X"
-2. **Descriptions should hook** — Write summaries that make people want to click
-3. **Use code blocks for technical content** — Syntax highlighting works automatically
-4. **Include images for visual interest** — Photos break up text and catch attention
-5. **Link to related posts** — Help readers discover more of your content
-
-### File Organization
-
-- **Use descriptive folder/file names** — `understanding-kubernetes-networking/` is better than `post1/`
-- **Group related assets** — If an article has images, keep them in the same folder
-- **Avoid special characters** — Stick to letters, numbers, hyphens, underscores
-- **Use lowercase** — Slugs should be lowercase (Hugo enforces this in URLs)
-
-### Images
-
-- **Optimal sizes**: Featured images 1200x600px or wider (16:9 aspect ratio)
-- **File formats**: JPG for photos, PNG for diagrams, WebP for web-optimized
-- **File size**: Keep images under 500KB (use compression tools)
-- **Naming**: Use lowercase, hyphens, descriptive names: `kubernetes-architecture.jpg`
-
-### Code Blocks
-
-Include a language identifier for syntax highlighting:
-
-````markdown
-```python
-def hello_world():
- print("Hello, world!")
-```
-````
-
-Supported languages: Python, Go, JavaScript, TypeScript, Bash, SQL, YAML, HTML, CSS, Rust, and many more.
-
----
-
-## Examples
-
-### Example 1: Tech Article with Images
-
-**File structure:**
-```
-content/articles/understanding-rest-apis/
-├── index.md
-├── rest-principles.png
-└── api-flow.png
-```
-
-**Front matter:**
-```toml
-+++
-title = "Understanding REST APIs: A Practical Guide"
-date = 2026-04-05T09:00:00Z
-draft = false
-type = "tech"
-featured = false
-image = "rest-principles.png"
-description = "REST APIs power the web. Learn the six constraints that make them scalable and how to build your own."
-+++
-```
-
-**Body:**
-```markdown
-REST (Representational State Transfer) is an architecture style for building web APIs...
-
-## The Six Constraints
-
-1. **Client-Server Architecture** — Separation of concerns...
-2. **Statelessness** — Each request contains all needed information...
-
-## Building Your First REST API
-
-Here's how to build a simple API with Go:
-
-```go
-package main
-
-import (
- "net/http"
-)
-
-func main() {
- http.HandleFunc("/api/hello", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("Hello, REST!"))
- })
- http.ListenAndServe(":8000", nil)
-}
-```
-
-...
-```
-
-### Example 2: Life Reflection
-
-**File: `content/articles/slowing-down.md`**
-
-```toml
-+++
-title = "On Slowing Down in a World That Won't"
-date = 2026-04-04T14:30:00Z
-draft = false
-type = "life"
-featured = false
-description = "There's a particular kind of exhaustion that comes from moving too fast. Here's what I learned about intentionality."
-+++
-```
-
-**Body:**
-```markdown
-Last week I caught myself reading the same sentence five times without understanding it. My mind was already on the next task, the next meeting, the next thing to fix.
-
-That's when it hit me: I'd been running on fumes for months...
-
-## What I'm Trying Now
-
-Three small changes have made a difference:
-
-1. **One deep work block per day** — No meetings, no emails, just focused time.
-2. **Walking between tasks** — A 5-minute walk breaks the mental loop.
-3. **Saying no more often** — Turns out, most requests weren't actually urgent.
-
-The irony is that slowing down made me more productive. Not because I did more, but because what I did actually mattered...
-```
-
-### Example 3: Quote Post
-
-**File: `content/articles/obstacle-is-the-way.md`**
-
-```toml
-+++
-title = "\"The Obstacle Is the Way\" — Marcus Aurelius"
-date = 2026-04-03T12:00:00Z
-draft = false
-type = "quote"
-featured = false
-description = "A powerful Stoic principle on turning problems into progress."
-+++
-```
-
-**Body:**
-```markdown
-> The impediment to action advances action. What stands in the way becomes the way.
->
-> — Marcus Aurelius, Meditations
-
-This quote resonates because it reframes failure and obstacles. Instead of seeing problems as roadblocks, Stoic philosophy teaches us to see them as *the path itself*.
-
-Every challenge I've faced that felt insurmountable at the time became the thing that taught me the most. The failed projects taught me more than the successes. The difficult conversations were more valuable than the easy ones.
-
-What obstacle are you facing right now? What if it's exactly what you need to learn next?
-```
-
-### Example 4: Link Post with Commentary
-
-**File: `content/articles/future-of-rust.md`**
-
-```toml
-+++
-title = "The Future of Rust in Systems Programming"
-date = 2026-04-02T10:00:00Z
-draft = false
-type = "link"
-featured = false
-image = "rust-logo.png"
-description = "Why Rust is becoming essential for systems programming and what's coming next."
-+++
-```
-
-**Body:**
-```markdown
-Read this excellent piece on [The Future of Rust in Systems Programming](https://example.com/rust-future)
-
-The author makes a compelling argument about why Rust's memory safety guarantees are becoming non-negotiable in infrastructure software. I particularly appreciated the section on how Rust adoption is accelerating in Linux kernel development.
-
-The key takeaway: safety isn't a trade-off anymore. Rust proves you can have performance *and* memory safety, which changes everything for systems programming.
-```
-
-### Example 5: Static Page (About)
-
-**File: `content/is/_index.md`** (already created, but here's how to edit it)
-
-```toml
-+++
-title = "About"
-date = 2026-01-01T00:00:00Z
-+++
-```
-
-**Body:**
-```markdown
-Hi, I'm Danilo. I'm an engineer and writer trying to figure out how to build good things and live well.
-
-## What I Write About
-
-- **Tech**: Systems design, Go, backend architecture, things I learn while building
-- **Life**: Reflections on work, learning, slowing down, and finding meaning
-- **Ideas**: Interesting thoughts and quotes that stick with me
-
-## What I'm Doing Now
-
-Currently building distributed systems and thinking about how to write better. I'm particularly interested in observability, performance, and the intersection of technology and philosophy.
-
-## Get in Touch
-
-Want to chat? [Send me a message](/is/here) — I read and reply to everything.
-```
-
----
-
-## File Format & Markdown
-
-### Standard Markdown
-
-Your content is written in Markdown. Here are the basics:
-
-```markdown
-# Heading 1
-## Heading 2
-### Heading 3
-
-**Bold text**
-*Italic text*
-
-- Bullet point
-- Another point
-
-1. Numbered item
-2. Another item
-
-[Link text](https://example.com)
-
-![Image alt text](image-file.jpg)
-
-> Blockquote text
-```
-
-### Code Blocks
-
-Fenced code blocks with language specification:
-
-````markdown
-```python
-print("Hello")
-```
-
-```javascript
-console.log("Hello");
-```
-
-```bash
-echo "Hello"
-```
-````
-
-### Images
-
-Place images in the same folder as the article:
-
-```markdown
-![A description of the image](image-filename.jpg)
-```
-
-For featured images in front matter, use just the filename:
-```toml
-image = "image-filename.jpg"
-```
-
----
-
-## Hugo Shortcodes
-
-Beyond standard Markdown, the theme includes special shortcodes for enhanced content. Place your content files (images, etc.) in the same folder as the article.
-
-### Responsive Image Shortcode
-
-The `img` shortcode creates responsive, optimized images with lazy loading, WebP format support, and a fade-in LQIP (Low Quality Image Placeholder) effect.
-
-**Usage:**
-```hugo
-{{< img src="image.jpg" alt="Image description" >}}
-```
-
-**Parameters:**
-- `src` (required) — Path to image file (relative to content folder)
-- `alt` (required) — Alt text for accessibility
-- `divClass` (optional) — CSS class to add to the wrapper div
-
-**Configuration:**
-Add to your `hugo.toml` to define responsive image sizes:
-```toml
-[params]
-imageSizes = [640, 900, 1200, 1600]
-```
-
-**Example:**
-```hugo
-{{< img src="kubernetes-architecture.jpg" alt="Kubernetes cluster architecture" >}}
-```
-
----
-
-### Quote Shortcode
-
-The `quote` shortcode creates styled blockquotes with optional source attribution and links.
-
-**Usage:**
-```hugo
-{{< quote source="Author Name" src="https://example.com" >}}
-This is a meaningful quote.
-{{< /quote >}}
-```
-
-**Parameters:**
-- Content between tags — The quote text
-- `source` (optional) — Author or source name
-- `src` (optional) — URL to link the source to (opens in new tab)
-
-**Examples:**
-
-Simple quote:
-```hugo
-{{< quote >}}
-The obstacle is the way.
-{{< /quote >}}
-```
-
-Quote with attribution:
-```hugo
-{{< quote source="Marcus Aurelius" >}}
-The impediment to action advances action. What stands in the way becomes the way.
-{{< /quote >}}
-```
-
-Quote with link:
-```hugo
-{{< quote source="Paul Graham" src="http://paulgraham.com/wealth.html" >}}
-The way to get rich is to work hard, at something people want.
-{{< /quote >}}
-```
-
----
-
-### Gravatar Shortcode
-
-The `gravatar` shortcode embeds a Gravatar profile image with optional caption and link.
-
-**Usage:**
-```hugo
-{{< gravatar mail="your@email.com" size=150 caption="My avatar" >}}
-```
-
-**Parameters:**
-- `mail` (optional) — Email address for Gravatar lookup (falls back to `author_email` from config)
-- `size` (optional) — Image size in pixels (default: 200)
-- `class` (optional) — CSS class for the figure element
-- `link` (optional) — URL to wrap the image in a link
-- `target` (optional) — Link target (_blank, _self, etc.)
-- `rel` (optional) — Link rel attribute (e.g., "author")
-- `caption` (optional) — Caption text (supports Markdown)
-- `alt` (optional) — Alt text (uses caption as fallback)
-
-**Configuration:**
-Add to your `hugo.toml` to set your default email:
-```toml
-[params]
-author_email = "your@email.com"
-```
-
-**Examples:**
-
-Simple gravatar with default size:
-```hugo
-{{< gravatar >}}
-```
-
-Custom size and caption:
-```hugo
-{{< gravatar mail="danilo@example.com" size=120 caption="That's me" >}}
-```
-
-With link to profile:
-```hugo
-{{< gravatar mail="danilo@example.com" size=150 link="https://gravatar.com/danilo" target="_blank" rel="author" caption="Click to view my profile" >}}
-```
-
----
-
-### Figure Shortcode
-
-The `figure` shortcode wraps images or other content in a semantic `<figure>` element with optional title, caption, attribution, and link. Use it to add context and styling around images.
-
-**Usage:**
-```hugo
-{{< figure caption="Image caption" attr="Photo by Someone" >}}
-{{< img src="image.jpg" alt="Description" >}}
-{{< /figure >}}
-```
-
-**Parameters:**
-- Content between tags — Inner content (typically an `img` shortcode)
-- `class` (optional) — CSS class for the figure element
-- `link` (optional) — URL to wrap the entire figure in a link
-- `target` (optional) — Link target (_blank, _self, etc.)
-- `rel` (optional) — Link rel attribute
-- `title` (optional) — Optional title displayed above the image
-- `caption` (optional) — Caption text (supports Markdown)
-- `attr` (optional) — Attribution text (supports Markdown)
-- `attrlink` (optional) — URL to link the attribution to
-
-**Examples:**
-
-Simple figure with caption:
-```hugo
-{{< figure caption="Kubernetes cluster architecture" >}}
-{{< img src="k8s-architecture.jpg" alt="Kubernetes cluster" >}}
-{{< /figure >}}
-```
-
-With title and attribution:
-```hugo
-{{< figure title="System Design" caption="A simplified view of the microservices architecture" attr="Diagram by John Doe" >}}
-{{< img src="architecture.jpg" alt="Architecture diagram" >}}
-{{< /figure >}}
-```
-
-With linked attribution:
-```hugo
-{{< figure caption="Beautiful sunset at Bryce Canyon" attr="Photo by Jane Smith" attrlink="https://example.com/jane" >}}
-{{< img src="bryce-canyon.jpg" alt="Sunset at Bryce Canyon" >}}
-{{< /figure >}}
-```
-
-Figure with clickable link:
-```hugo
-{{< figure link="https://example.com/full-image" target="_blank" caption="Click to view full resolution" >}}
-{{< img src="preview.jpg" alt="Preview image" >}}
-{{< /figure >}}
-```
-
----
-
-### Video Shortcode
-
-The `video` shortcode embeds HTML5 videos with controls, optional autoplay, looping, and muting. Supports mp4, webm, and other HTML5 video formats.
-
-**Usage:**
-```hugo
-{{< video src="my-video.mp4" width=600 height=400 >}}
-```
-
-**Parameters:**
-- `src` (required) — Path to video file (mp4, webm, etc.)
-- `width` (optional) — Video width in pixels
-- `height` (optional) — Video height in pixels
-- `autoplay` (optional) — true/false (default: false)
-- `loop` (optional) — true/false (default: false)
-- `muted` (optional) — true/false (default: false)
-- `class` (optional) — CSS class for styling
-
-**Note:** The `muted` attribute is often required for autoplay to work in modern browsers.
-
-**Examples:**
-
-Simple video with controls:
-```hugo
-{{< video src="demo.mp4" width=800 height=450 >}}
-```
-
-Auto-looping muted video (useful for GIF-like effects):
-```hugo
-{{< video src="animation.mp4" width=640 height=480 autoplay=true loop=true muted=true >}}
-```
-
-Styled video with custom class:
-```hugo
-{{< video src="tutorial.webm" width=1024 height=576 class="responsive-video" >}}
-```
-
----
-
-### Text Formatting Shortcodes
-
-#### Strike Shortcode
-
-Creates strikethrough text.
-
-**Usage:**
-```hugo
-{{< strike >}}This text is crossed out{{< /strike >}}
-```
-
-#### Highlight (em) Shortcode
-
-Creates highlighted/marked text (yellow background).
-
-**Usage:**
-```hugo
-{{< em >}}This text is highlighted{{< /em >}}
-```
-
-#### Dropcap Shortcode
-
-Creates a paragraph with a decorative drop cap (large first letter).
-
-**Usage:**
-```hugo
-{{< dropcap >}}
-Once upon a time, there was a beautiful story that began with this first letter...
-{{< /dropcap >}}
-```
-
-**Parameters:**
-- `class` (optional) — CSS class for styling
-
----
-
-### Container Shortcodes
-
-#### Div Shortcode
-
-Opens a `<div>` wrapper with optional CSS class. Pair with `div-close` shortcode.
-
-**Usage:**
-```hugo
-{{< div class="container featured" >}}
-Content inside the div
-{{< div-close >}}
-```
-
-**Parameters:**
-- `class` — CSS class(es) for the div element
-
-#### Div-Close Shortcode
-
-Closes a div opened with the `div` shortcode.
-
----
-
-### Gallery Shortcodes
-
-#### Gallery Shortcode
-
-Creates a gallery container for displaying multiple images.
-
-**Usage:**
-```hugo
-{{< gallery >}}
-{{< gal-img src="photo1.jpg" alt="Photo 1" >}}
-{{< gal-img src="photo2.jpg" alt="Photo 2" >}}
-{{< /gallery >}}
-```
-
-#### Gallery Image (gal-img) Shortcode
-
-Embeds an image in a gallery. Must be used inside a `gallery` shortcode.
-
-**Usage:**
-```hugo
-{{< gal-img src="image.jpg" alt="Image description" >}}
-```
-
-**Parameters:**
-- `src` (required) — Path to image file
-- `alt` (required) — Alt text for accessibility
-
----
-
-### Utility Shortcodes
-
-#### SVG Shortcode
-
-Embeds inline SVG icons from your `assets/SVGs/` folder.
-
-**Usage:**
-```hugo
-{{< svg name="icon-name" class="optional-class" >}}
-```
-
-**Parameters:**
-- `name` (required) — Name of SVG file (without .svg extension). File must be in `assets/SVGs/` folder.
-- `class` (optional) — CSS class for styling the icon
-
-**Example:**
-```hugo
-{{< svg name="github" class="social-icon" >}}
-```
-
-#### Actions Shortcode
-
-Creates a button-style action link (useful for CTAs).
-
-**Usage:**
-```hugo
-{{< actions url="https://example.com" desc="Click Me" outclass="optional-wrapper-class" inclass="optional-button-class" >}}
-```
-
-**Parameters:**
-- `url` (required) — Link URL
-- `desc` (required) — Button text
-- `outclass` (optional) — CSS class for the `<ul>` wrapper
-- `inclass` (optional) — CSS class for the `<a>` button element
-
-**Example:**
-```hugo
-{{< actions url="https://github.com/danix2" desc="Visit my GitHub" outclass="centered" inclass="primary" >}}
-```
-
----
-
-## Troubleshooting
-
-### Article doesn't appear on site
-
-- **Check `draft = false`** — Draft articles don't publish
-- **Check the date** — Articles with future dates are treated as drafts
-- **Check `type`** — Articles need a valid type (tech, life, quote, link, photo)
-- **Check folder structure** — Articles must be in `content/articles/`
-
-### Image doesn't show
-
-- **Relative paths only** — Use filename only (`image.jpg`) or relative paths (`./images/image.jpg`)
-- **Place in same folder** — If in a folder article, put the image in the same folder as `index.md`
-- **Check the path** — Make sure the filename matches exactly (case-sensitive on Linux)
-
-### Feed shows wrong description
-
-- **Explicit is better** — Always set `description` in front matter instead of relying on auto-summary
-- **Keep it short** — Max 2 lines (~150 characters)
-- **Write for scanners** — People decide whether to click based on the description
-
-### Featured post not showing
-
-- **Only one per site** — If multiple articles have `featured = true`, only one displays
-- **Check date order** — The featured post shows first, then newest articles below
-- **Remove other featured flags** — Make sure only one article has `featured = true`
-
----
-
-## Quick Reference
-
-### Create New Article
-```bash
-hugo new articles/my-article-title/index.md
-# Edit the file, add front matter, write content
-# Set draft = false when ready to publish
-```
-
-### Create New Static Page
-```bash
-hugo new is/page-name.md
-# Edit and publish like articles
-```
-
-### Preview Locally
-```bash
-hugo server
-# Open http://localhost:1313
-```
-
-### Commit and Deploy
-```bash
-git add content/
-git commit -m "content: add [article title]"
-git push origin master
-```
-
-### Content Types at a Glance
-
-| Type | Badge | Use For |
-|------|-------|---------|
-| `tech` | Purple | Technical articles, tutorials, engineering |
-| `life` | Amber | Personal reflections, life lessons |
-| `quote` | Green | Meaningful quotes with context |
-| `link` | Cyan | Interesting links with commentary |
-| `photo` | Pink | Visual content and stories |
-
----
-
-## Questions?
-
-Refer back to this guide whenever you:
-- Create new content
-- Edit existing articles
-- Reorganize the site
-- Add images or code examples
-- Publish or unpublish articles
-
-Happy writing! 🚀
diff --git a/archetypes/article.md b/archetypes/article.md
deleted file mode 100644
index 477a18f..0000000
--- a/archetypes/article.md
+++ /dev/null
@@ -1,11 +0,0 @@
-+++
-title = "{{ replace .File.ContentBaseName "-" " " | title }}"
-date = {{ .Date }}
-draft = true
-type = "tech" # Options: tech, life, quote, link, photo
-featured = false
-image = "" # Optional: image path or URL
-description = "Brief description of the article (2 lines max)"
-+++
-
-Write your article here...
diff --git a/archetypes/default.md b/archetypes/default.md
deleted file mode 100644
index 502fd89..0000000
--- a/archetypes/default.md
+++ /dev/null
@@ -1,7 +0,0 @@
-+++
-title = "{{ replace .File.ContentBaseName "-" " " | title }}"
-date = {{ .Date }}
-draft = true
-+++
-
-Content here...
diff --git a/archetypes/page.md b/archetypes/page.md
deleted file mode 100644
index fe37a38..0000000
--- a/archetypes/page.md
+++ /dev/null
@@ -1,8 +0,0 @@
-+++
-title = "{{ replace .File.ContentBaseName "-" " " | title }}"
-date = {{ .Date }}
-draft = false
-type = "page"
-+++
-
-Write your page content here...
diff --git a/assets/css/chroma-custom.css b/assets/css/chroma-custom.css
new file mode 100644
index 0000000..d632e09
--- /dev/null
+++ b/assets/css/chroma-custom.css
@@ -0,0 +1,391 @@
+/* === Chroma Custom — Catppuccin Macchiato === */
+
+:root {
+ /* Catppuccin Macchiato palette */
+ --ctp-base: #24273a;
+ --ctp-surface0: #363a4f;
+ --ctp-surface1: #494d64;
+ --ctp-overlay0: #6e738d;
+ --ctp-text: #cad3f5;
+ --ctp-subtext1: #b8c0e0;
+ --ctp-lavender: #b7bdf8;
+ --ctp-blue: #8aadf4;
+ --ctp-sapphire: #7dc4e4;
+ --ctp-sky: #91d7e3;
+ --ctp-teal: #8bd5ca;
+ --ctp-green: #a6da95;
+ --ctp-yellow: #eed49f;
+ --ctp-peach: #f5a97f;
+ --ctp-maroon: #ee99a0;
+ --ctp-red: #ed8796;
+ --ctp-mauve: #c6a0f6;
+ --ctp-pink: #f5bde6;
+}
+
+/* === Code block wrapper and header bar === */
+
+.code-block-wrapper {
+ margin: 1.5rem 0;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ border: 1px solid var(--border);
+}
+
+.code-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0.375rem 0.75rem;
+ background-color: var(--ctp-surface0);
+ border-bottom: 1px solid var(--ctp-surface1);
+ font-family: 'JetBrains Mono', monospace;
+ font-size: 0.75rem;
+}
+
+.code-lang-label {
+ color: var(--ctp-subtext1);
+ letter-spacing: 0.05em;
+}
+
+.code-copy-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+}
+
+.code-copy-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ padding: 0.25rem;
+ border-radius: 0.25rem;
+ color: var(--ctp-overlay0);
+ transition: color 0.15s ease, background 0.15s ease;
+ line-height: 1;
+}
+
+.code-copy-btn:hover {
+ color: var(--ctp-text);
+ background: rgba(202, 211, 245, 0.08);
+}
+
+.code-copy-btn:focus-visible {
+ outline: 2px solid var(--ctp-lavender);
+ outline-offset: 2px;
+}
+
+.code-copy-btn [data-feather],
+.code-copy-btn svg {
+ width: 14px !important;
+ height: 14px !important;
+ stroke-width: 2px;
+}
+
+.code-copy-btn .hidden {
+ display: none;
+}
+
+.code-copy-btn.is-copied {
+ color: var(--ctp-green);
+}
+
+/* === Code body === */
+
+.code-body {
+ overflow-x: auto;
+}
+
+.code-body .highlight,
+.code-body .highlight .chroma {
+ margin: 0;
+ border-radius: 0;
+ border: none;
+ background-color: var(--ctp-base);
+}
+
+/* === Reset conflicts with main.css base styles === */
+
+.code-block-wrapper pre,
+.prose .code-block-wrapper pre,
+.prose-invert .code-block-wrapper pre {
+ margin: 0;
+ padding: 0;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ overflow-x: visible;
+}
+
+/* Re-apply padding after the reset above, with higher specificity */
+.code-block-wrapper .highlight .lntd:first-child pre {
+ padding: 0.875rem 0;
+}
+
+.code-block-wrapper .highlight .lntd:last-child pre {
+ padding: 0.875rem 1rem;
+}
+
+/* === Chroma table layout (lineNumbersInTable = true) === */
+
+.highlight table {
+ border-collapse: collapse;
+ width: 100%;
+}
+
+.highlight td {
+ padding: 0;
+ vertical-align: top;
+}
+
+/* Line number column — not selectable */
+/* color: --ctp-overlay0 (#6e738d) on --ctp-surface0 (#363a4f) = ~3.5:1 */
+.highlight .lnt,
+.highlight .ln {
+ padding: 0.875rem 0.75rem 0.875rem 1rem;
+ color: var(--ctp-overlay0);
+ user-select: none;
+ -webkit-user-select: none;
+ min-width: 2.5rem;
+ text-align: right;
+ border-right: 1px solid var(--ctp-surface1);
+ font-size: 0.8125rem;
+}
+
+.highlight .lntd:first-child {
+ background-color: var(--ctp-surface0);
+}
+
+.highlight .lntd:first-child pre {
+ padding: 0.875rem 0;
+}
+
+.highlight .lntd:last-child pre {
+ padding: 0.875rem 1rem;
+}
+
+.highlight .chroma {
+ overflow-x: auto;
+}
+
+.code-block-wrapper pre,
+.code-block-wrapper code {
+ font-size: 0.875em;
+}
+
+/* === Syntax token colors (dark theme default) === */
+
+.highlight {
+ background-color: var(--ctp-base);
+ color: var(--ctp-text);
+}
+
+/* Keywords — mauve */
+.highlight .k,
+.highlight .kc,
+.highlight .kd,
+.highlight .kn,
+.highlight .kp,
+.highlight .kr,
+.highlight .kt {
+ color: var(--ctp-mauve);
+ font-weight: 500;
+}
+
+/* Strings — green */
+.highlight .s,
+.highlight .sa,
+.highlight .sb,
+.highlight .sc,
+.highlight .dl,
+.highlight .sd,
+.highlight .s1,
+.highlight .s2,
+.highlight .se,
+.highlight .sh,
+.highlight .si,
+.highlight .sx,
+.highlight .sr,
+.highlight .ss {
+ color: var(--ctp-green);
+}
+
+/* Numbers — peach */
+.highlight .m,
+.highlight .mb,
+.highlight .mf,
+.highlight .mh,
+.highlight .mi,
+.highlight .il,
+.highlight .mo {
+ color: var(--ctp-peach);
+}
+
+/* Comments — overlay0, italic */
+.highlight .c,
+.highlight .c1,
+.highlight .cm,
+.highlight .cs,
+.highlight .cp,
+.highlight .cpf {
+ color: var(--ctp-overlay0);
+ font-style: italic;
+}
+
+/* Operators — sky */
+.highlight .o,
+.highlight .ow {
+ color: var(--ctp-sky);
+}
+
+/* Names / Identifiers */
+.highlight .n {
+ color: var(--ctp-text);
+}
+
+.highlight .na {
+ color: var(--ctp-yellow);
+}
+
+.highlight .nb {
+ color: var(--ctp-blue);
+}
+
+.highlight .nc {
+ color: var(--ctp-yellow);
+}
+
+.highlight .nd {
+ color: var(--ctp-pink);
+}
+
+.highlight .ne {
+ color: var(--ctp-maroon);
+}
+
+.highlight .nf,
+.highlight .fm {
+ color: var(--ctp-blue);
+}
+
+.highlight .ni {
+ color: var(--ctp-text);
+}
+
+.highlight .nl {
+ color: var(--ctp-teal);
+}
+
+.highlight .nn {
+ color: var(--ctp-yellow);
+}
+
+.highlight .nt {
+ color: var(--ctp-mauve);
+}
+
+.highlight .nv,
+.highlight .vc,
+.highlight .vg,
+.highlight .vi {
+ color: var(--ctp-text);
+}
+
+/* Punctuation */
+.highlight .p {
+ color: var(--ctp-subtext1);
+}
+
+/* Generic tokens (diff output etc.) */
+.highlight .gd {
+ color: var(--ctp-red);
+ background: rgba(237, 135, 150, 0.1);
+}
+
+.highlight .gi {
+ color: var(--ctp-green);
+ background: rgba(166, 218, 149, 0.1);
+}
+
+.highlight .gh {
+ color: var(--ctp-lavender);
+ font-weight: bold;
+}
+
+.highlight .gu {
+ color: var(--ctp-overlay0);
+}
+
+.highlight .ge {
+ font-style: italic;
+}
+
+.highlight .gs {
+ font-weight: bold;
+}
+
+/* Error */
+.highlight .err {
+ color: var(--ctp-red);
+}
+
+/* === Light theme overrides (Catppuccin Latte) === */
+
+html.theme-light .code-block-wrapper {
+ border-color: #ccd0da;
+}
+
+html.theme-light .code-header {
+ background-color: #dce0ea;
+ border-bottom-color: #bcc0cc;
+}
+
+html.theme-light .code-lang-label {
+ color: #5c5f77;
+}
+
+html.theme-light .code-copy-btn {
+ color: #6c6f85;
+}
+
+html.theme-light .code-copy-btn:hover {
+ color: #4c4f69;
+ background: rgba(76, 79, 105, 0.08);
+}
+
+html.theme-light .code-body .highlight,
+html.theme-light .code-body .highlight .chroma {
+ background-color: #eff1f5;
+ color: #4c4f69;
+}
+
+html.theme-light .highlight .lntd:first-child {
+ background-color: #e6e9ef;
+}
+
+html.theme-light .highlight .lnt,
+html.theme-light .highlight .ln {
+ color: #bcc0cc;
+ border-right-color: #bcc0cc;
+}
+
+/* Comments must be darker to read on light bg */
+html.theme-light .highlight .c,
+html.theme-light .highlight .c1,
+html.theme-light .highlight .cm,
+html.theme-light .highlight .cs,
+html.theme-light .highlight .cp,
+html.theme-light .highlight .cpf {
+ color: #7c7f93;
+}
+
+/* Text defaults */
+html.theme-light .highlight .n {
+ color: #4c4f69;
+}
+
+html.theme-light .highlight .p {
+ color: #6c6f85;
+}
diff --git a/assets/css/components/404.css b/assets/css/components/404.css
deleted file mode 100644
index 3db099f..0000000
--- a/assets/css/components/404.css
+++ /dev/null
@@ -1,247 +0,0 @@
-/* 404.css */
-
-.hero--404 {
- position: relative;
- min-height: 100vh;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 2rem;
-}
-
-.content-wrapper {
- display: grid;
- grid-template-columns: 65% 35%;
- gap: 3rem;
- max-width: 1200px;
- width: 100%;
-}
-
-@media (max-width: 900px) {
- .content-wrapper {
- grid-template-columns: 1fr;
- gap: 2rem;
- }
-}
-
-/* Quote Section */
-.quote-section {
- margin-bottom: 3rem;
- padding: 2rem;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 8px;
- position: relative;
-}
-
-.quote-mark {
- font-size: 4rem;
- color: var(--accent);
- opacity: 0.3;
- margin-bottom: -0.5rem;
-}
-
-.quote-text {
- font-family: var(--font-body);
- font-size: 1.3rem;
- font-weight: 400;
- line-height: 1.8;
- color: var(--text);
- margin-bottom: 1rem;
-}
-
-.quote-author {
- font-family: var(--font-mono);
- font-size: 0.85rem;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- color: var(--text-dim);
-}
-
-/* Search Box */
-.search-box {
- display: flex;
- gap: 0;
- margin-bottom: 3rem;
- overflow: hidden;
- border-radius: 4px;
- border: 1px solid var(--border);
-}
-
-.search-box input {
- flex: 1;
- padding: 0.75rem 1rem;
- background: var(--surface);
- border: none;
- color: var(--text);
- font-family: var(--font-mono);
- font-size: 0.9rem;
-}
-
-.search-box input::placeholder {
- color: var(--text-dim);
-}
-
-.search-box button {
- padding: 0.75rem 1rem;
- background: var(--accent);
- border: none;
- color: #000;
- cursor: pointer;
- transition: all var(--duration-base) ease;
-}
-
-.search-box button:hover {
- background: var(--accent2);
-}
-
-/* Quick Nav */
-.quick-nav {
- margin-bottom: 2rem;
-}
-
-.quick-nav h3 {
- font-size: 1rem;
- margin-bottom: 1rem;
-}
-
-.quick-nav ul {
- list-style: none;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.quick-nav a {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.75rem 1rem;
- border: 1px solid var(--border);
- border-radius: 4px;
- color: var(--text);
- text-decoration: none;
- transition: all var(--duration-base) ease;
-}
-
-.quick-nav a:hover {
- border-color: var(--accent);
- background: rgba(168, 85, 247, 0.05);
- padding-left: 1.25rem;
-}
-
-/* Right Column */
-.hero-right-404 {
- display: flex;
- flex-direction: column;
- gap: 2rem;
-}
-
-.recent-articles,
-.terminal-widget {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 8px;
- overflow: hidden;
-}
-
-.recent-articles h3,
-.terminal-widget .terminal-bar {
- padding: 1rem;
- background: var(--bg2);
- font-size: 0.9rem;
- font-weight: 700;
-}
-
-.terminal-widget .terminal-bar {
- display: flex;
- gap: 0.75rem;
- align-items: center;
-}
-
-.terminal-dot {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- display: inline-block;
-}
-
-.recent-articles ul {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-.recent-articles li {
- border-bottom: 1px solid var(--border);
- padding: 0;
- margin: 0;
-}
-
-.recent-articles li:last-child {
- border-bottom: none;
-}
-
-.article-link {
- display: block;
- padding: 0.75rem 1rem;
- color: var(--text);
- text-decoration: none;
- font-size: 0.9rem;
- transition: all var(--duration-base) ease;
- border-left: 2px solid transparent;
-}
-
-.article-link:hover {
- background: rgba(168, 85, 247, 0.05);
- border-left-color: var(--accent);
- padding-left: 1.25rem;
-}
-
-.article-link[data-type="tech"] {
- color: var(--type-tech);
-}
-
-.article-link[data-type="life"] {
- color: var(--type-life);
-}
-
-.article-link[data-type="quote"] {
- color: var(--type-quote);
-}
-
-.article-link[data-type="link"] {
- color: var(--type-link);
-}
-
-.article-link[data-type="photo"] {
- color: var(--type-photo);
-}
-
-.terminal-content {
- padding: 1rem;
- font-family: var(--font-mono);
- font-size: 0.75rem;
- color: var(--terminal-text);
- line-height: 1.6;
-}
-
-.terminal-content div {
- white-space: pre-wrap;
- word-wrap: break-word;
-}
-
-.terminal-prompt {
- color: var(--terminal-prompt);
-}
-
-#terminal-files {
- margin-top: 0.5rem;
- color: var(--terminal-accent, #38bdf8);
-}
-
-@media (prefers-reduced-motion: reduce) {
- .article-link:hover {
- padding-left: 1rem;
- }
-}
diff --git a/assets/css/components/article-hero.css b/assets/css/components/article-hero.css
deleted file mode 100644
index cc9e180..0000000
--- a/assets/css/components/article-hero.css
+++ /dev/null
@@ -1,225 +0,0 @@
-/* article-hero.css */
-
-.article-hero {
- position: relative;
- min-height: 400px;
- background-size: cover;
- background-position: center;
- display: flex;
- align-items: flex-end;
- padding: 3rem 2rem;
- margin-bottom: 2rem;
-}
-
-.article-hero-overlay {
- position: absolute;
- inset: 0;
- background: linear-gradient(180deg, rgba(6, 11, 16, 0.3) 0%, rgba(6, 11, 16, 0.8) 100%);
-}
-
-.article-hero-content {
- position: relative;
- z-index: 2;
- width: 100%;
- max-width: var(--container-max);
- margin: 0 auto;
- padding: 0 1.5rem;
-}
-
-.article-breadcrumb {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- font-family: var(--font-mono);
- font-size: 0.85rem;
- color: var(--text-dim);
- margin-bottom: 1.5rem;
-}
-
-.article-breadcrumb a {
- color: var(--accent);
- text-decoration: none;
- transition: color var(--duration-base) ease;
-}
-
-.article-breadcrumb a:hover {
- color: var(--accent2);
-}
-
-.article-hero-content h1 {
- font-size: clamp(2rem, 6vw, 3.5rem);
- margin: 0;
-}
-
-.article-meta-bar {
- position: sticky;
- top: 0;
- z-index: 50;
- background: rgba(6, 11, 16, 0.95);
- backdrop-filter: blur(4px);
- padding: 1rem 2rem;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-
-.article-meta {
- display: flex;
- align-items: center;
- gap: 1.5rem;
- font-size: 0.85rem;
- font-family: var(--font-mono);
- text-transform: uppercase;
- letter-spacing: 0.08em;
-}
-
-.article-type-badge {
- padding: 0.4rem 0.8rem;
- border-radius: 4px;
- font-weight: 600;
- background: color-mix(in srgb, currentColor 15%, transparent);
- border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
-}
-
-.article-type-badge.type-tech {
- color: var(--type-tech);
-}
-
-.article-type-badge.type-life {
- color: var(--type-life);
-}
-
-.article-type-badge.type-quote {
- color: var(--type-quote);
-}
-
-.article-type-badge.type-link {
- color: var(--type-link);
-}
-
-.article-type-badge.type-photo {
- color: var(--type-photo);
-}
-
-.article-date,
-.article-read-time {
- color: var(--text-dim);
-}
-
-/* Article Body */
-.article-body {
- padding: 2rem 1.5rem;
- max-width: 800px;
- margin: 0 auto;
-}
-
-/* Article Footer Nav */
-.article-footer-nav {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 2rem;
- padding: 2rem;
- max-width: var(--container-max);
- margin: 3rem auto 0;
- border-top: 1px solid var(--border);
-}
-
-@media (max-width: 768px) {
- .article-footer-nav {
- grid-template-columns: 1fr;
- }
-}
-
-.nav-prev,
-.nav-next {
- padding: 1.5rem;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 4px;
- text-decoration: none;
- transition: all var(--duration-base) ease;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.nav-prev:hover,
-.nav-next:hover {
- border-color: var(--accent);
- background: rgba(168, 85, 247, 0.05);
- transform: translateY(-2px);
-}
-
-.nav-label {
- font-family: var(--font-mono);
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.1em;
- color: var(--accent);
-}
-
-.nav-title {
- font-family: var(--font-head);
- font-size: 1.1rem;
- font-weight: 700;
- color: var(--text);
-}
-
-/* Static Page */
-.page-hero {
- position: relative;
- height: 40vh;
- background: linear-gradient(135deg, var(--accent), var(--accent2));
- display: flex;
- align-items: flex-end;
- padding: 3rem 2rem;
- margin-bottom: 2rem;
-}
-
-.page-hero-overlay {
- position: absolute;
- inset: 0;
- background: rgba(6, 11, 16, 0.3);
-}
-
-.page-hero-content {
- position: relative;
- z-index: 2;
- width: 100%;
- max-width: var(--container-max);
- margin: 0 auto;
- padding: 0 1.5rem;
-}
-
-.page-hero-content h1 {
- font-size: clamp(2rem, 5vw, 3rem);
- margin: 0;
-}
-
-.page-content {
- padding: 2rem 1.5rem;
-}
-
-.page-nav ul {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.page-nav a {
- padding: 0.75rem 1rem;
- border-radius: 4px;
- transition: all var(--duration-base) ease;
- display: block;
-}
-
-.page-nav a:hover {
- background: var(--surface);
- padding-left: 1.25rem;
-}
-
-.page-nav a.active {
- font-weight: 700;
- color: var(--accent);
-}
diff --git a/assets/css/components/card.css b/assets/css/components/card.css
deleted file mode 100644
index 3d218bc..0000000
--- a/assets/css/components/card.css
+++ /dev/null
@@ -1,254 +0,0 @@
-/* card.css */
-.post-card {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 8px;
- overflow: hidden;
- transition: var(--transition);
- display: flex;
- gap: var(--gap-md);
-}
-
-.post-card:hover {
- border-color: var(--accent);
- box-shadow: 0 0 20px rgba(168, 85, 247, 0.2);
-}
-
-.post-card-image {
- width: 120px;
- height: 100px;
- flex-shrink: 0;
- object-fit: cover;
- background: linear-gradient(135deg, var(--border), var(--bg2));
- border-right: 1px solid var(--border);
-}
-
-@media (min-width: 768px) {
- .post-card-image {
- width: 160px;
- height: 120px;
- }
-}
-
-.post-card-body {
- padding: var(--gap-md);
- display: flex;
- flex-direction: column;
- justify-content: center;
- flex-grow: 1;
-}
-
-.post-type-badge {
- display: inline-block;
- font-family: var(--font-mono);
- font-size: var(--fs-badge);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.1em;
- padding: 0.35rem 0.7rem;
- border-radius: 20px;
- margin-bottom: 0.5rem;
- width: fit-content;
-}
-
-/* Type-specific badge colors */
-.post-type-badge.tech {
- background: color-mix(in srgb, var(--type-tech) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--type-tech) 30%, transparent);
- color: var(--type-tech);
-}
-
-.post-type-badge.life {
- background: color-mix(in srgb, var(--type-life) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--type-life) 30%, transparent);
- color: var(--type-life);
-}
-
-.post-type-badge.quote {
- background: color-mix(in srgb, var(--type-quote) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--type-quote) 30%, transparent);
- color: var(--type-quote);
-}
-
-.post-type-badge.link {
- background: color-mix(in srgb, var(--type-link) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--type-link) 30%, transparent);
- color: var(--type-link);
-}
-
-.post-type-badge.photo {
- background: color-mix(in srgb, var(--type-photo) 15%, transparent);
- border: 1px solid color-mix(in srgb, var(--type-photo) 30%, transparent);
- color: var(--type-photo);
-}
-
-.post-card-title {
- font-family: var(--font-head);
- font-size: 1.1rem;
- font-weight: 700;
- line-height: 1.3;
- margin-bottom: 0.5rem;
- color: var(--text);
-}
-
-.post-card-title a {
- color: var(--text);
- text-decoration: none;
-}
-
-.post-card-title a:hover {
- color: var(--accent);
-}
-
-.post-card-excerpt {
- color: var(--text-dim);
- font-size: 0.9rem;
- line-height: 1.6;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
- margin-bottom: 0.5rem;
-}
-
-.post-card-meta {
- display: flex;
- gap: 1rem;
- font-family: var(--font-mono);
- font-size: 0.75rem;
- color: var(--muted);
- text-transform: uppercase;
- letter-spacing: 0.08em;
-}
-
-/* Featured card in grid */
-.post-card.featured {
- grid-row: span 2;
-}
-
-.post-card.featured .post-card-image {
- width: 100%;
- height: 180px;
- border-right: none;
- border-bottom: 1px solid var(--border);
-}
-
-.post-card.featured .post-card-body {
- padding: var(--gap-lg);
-}
-
-@media (max-width: 768px) {
- .post-card {
- flex-direction: column;
- }
-
- .post-card-image {
- width: 100%;
- height: 150px;
- border-right: none;
- border-bottom: 1px solid var(--border);
- }
-
- .post-card.featured .post-card-image {
- height: 150px;
- }
-}
-
-/* ─── Homepage article cards ─────────────────────────────── */
-
-.articles-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
- gap: 1.5rem;
- margin-bottom: 2rem;
-}
-
-@media (max-width: 768px) {
- .articles-grid { grid-template-columns: 1fr; }
-}
-
-.article-card {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 0;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- transition: var(--transition);
-}
-
-.article-card:hover {
- transform: translate(0, -4px);
- box-shadow: 0 8px 30px var(--accent-glow);
- border-color: var(--accent);
-}
-
-/* Badge background set via inline style in template using var(--type-*) */
-.article-type {
- font-family: var(--font-mono);
- font-size: var(--fs-badge);
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.1em;
- padding: 0.4rem 1rem;
- color: var(--bg);
- width: fit-content;
- border-radius: 3px;
- margin: 1rem 1rem 0;
-}
-
-.article-content {
- padding: 1rem 1rem 1.5rem;
- display: flex;
- flex-direction: column;
- flex: 1;
-}
-
-.article-title {
- font-family: var(--font-head);
- font-size: 1.1rem;
- font-weight: 700;
- line-height: 1.3;
- margin-bottom: 0.5rem;
- letter-spacing: -0.025em;
-}
-
-.article-title a { color: var(--text); text-decoration: none; }
-.article-title a:hover { color: var(--accent); }
-
-.article-excerpt {
- color: var(--text-dim);
- font-size: 0.9rem;
- line-height: 1.6;
- flex: 1;
- margin-bottom: 1rem;
- display: -webkit-box;
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-.article-meta {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-top: 1rem;
- border-top: 1px solid var(--border);
- font-family: var(--font-mono);
- font-size: 0.75rem;
- color: var(--muted);
- text-transform: uppercase;
- letter-spacing: 0.08em;
- margin-top: auto;
-}
-
-/* .article-read named distinctly from .article-link (used in 404.css) */
-.article-read {
- color: var(--accent);
- text-decoration: none;
- font-weight: 600;
- letter-spacing: 0.05em;
- transition: color var(--duration-fast) ease;
-}
-
-.article-read:hover { color: var(--accent2); }
diff --git a/assets/css/components/code.css b/assets/css/components/code.css
deleted file mode 100644
index 4cdc57b..0000000
--- a/assets/css/components/code.css
+++ /dev/null
@@ -1,138 +0,0 @@
-/* code.css */
-pre {
- background: var(--surface);
- border: 1px solid var(--border);
- border-left: 3px solid var(--accent);
- border-radius: 6px;
- padding: 1rem;
- overflow-x: auto;
- position: relative;
- margin: 1.5rem 0;
-}
-
-pre code {
- font-family: var(--font-mono);
- font-size: 0.9rem;
- line-height: 1.6;
- color: var(--text);
-}
-
-/* Chroma syntax highlighting */
-.highlight {
- background: var(--surface);
- border: 1px solid var(--border);
- border-left: 3px solid var(--accent);
- border-radius: 6px;
- padding: 1rem;
- overflow-x: auto;
- margin: 1.5rem 0;
- position: relative;
-}
-
-.highlight code {
- background: none;
- padding: 0;
- border-radius: 0;
- color: inherit;
-}
-
-/* Chroma color overrides for dark theme */
-.highlight .k { color: #f59e0b; }
-.highlight .kn { color: #f59e0b; }
-.highlight .kp { color: #f59e0b; }
-.highlight .kr { color: #f59e0b; }
-.highlight .kt { color: #a855f7; }
-.highlight .n { color: #c4d6e8; }
-.highlight .na { color: #38bdf8; }
-.highlight .nb { color: #38bdf8; }
-.highlight .nc { color: #a855f7; }
-.highlight .no { color: #00ff88; }
-.highlight .nd { color: #f59e0b; }
-.highlight .ni { color: #a855f7; }
-.highlight .ne { color: #f59e0b; }
-.highlight .nf { color: #38bdf8; }
-.highlight .nl { color: #a855f7; }
-.highlight .nn { color: #a855f7; }
-.highlight .nt { color: #f59e0b; }
-.highlight .nv { color: #c4d6e8; }
-.highlight .s { color: #00ff88; }
-.highlight .sa { color: #00ff88; }
-.highlight .sb { color: #00ff88; }
-.highlight .sc { color: #00ff88; }
-.highlight .s1 { color: #00ff88; }
-.highlight .s2 { color: #00ff88; }
-.highlight .se { color: #f59e0b; }
-.highlight .sh { color: #00ff88; }
-.highlight .si { color: #f59e0b; }
-.highlight .sx { color: #00ff88; }
-.highlight .sr { color: #00ff88; }
-.highlight .ss { color: #00ff88; }
-.highlight .m { color: #38bdf8; }
-.highlight .mb { color: #38bdf8; }
-.highlight .mf { color: #38bdf8; }
-.highlight .mh { color: #38bdf8; }
-.highlight .mi { color: #38bdf8; }
-.highlight .il { color: #38bdf8; }
-.highlight .mo { color: #38bdf8; }
-.highlight .o { color: #c4d6e8; }
-.highlight .ow { color: #a855f7; }
-.highlight .c { color: #7a9bb8; }
-.highlight .c1 { color: #7a9bb8; }
-.highlight .ch { color: #7a9bb8; }
-.highlight .cm { color: #7a9bb8; }
-.highlight .cp { color: #f59e0b; }
-.highlight .cpf { color: #f59e0b; }
-
-html.theme-light .highlight {
- background: var(--surface);
- border-left-color: var(--accent);
-}
-
-html.theme-light .highlight .c { color: #6a7fa0; }
-
-/* Copy button for code blocks */
-.code-copy-btn {
- position: absolute;
- top: 0.75rem;
- right: 0.75rem;
- background: var(--accent);
- color: #fff;
- border: none;
- padding: 0.4rem 0.8rem;
- border-radius: 4px;
- font-family: var(--font-mono);
- font-size: 0.75rem;
- cursor: pointer;
- opacity: 0;
- transition: var(--transition);
- text-transform: uppercase;
- letter-spacing: 0.08em;
-}
-
-.highlight:hover .code-copy-btn,
-pre:hover .code-copy-btn {
- opacity: 1;
-}
-
-.code-copy-btn:hover {
- background: var(--accent2);
- color: var(--bg);
-}
-
-.code-copy-btn.copied {
- background: var(--accent2);
-}
-
-/* Inline code */
-code {
- font-family: var(--font-mono);
- background: var(--surface);
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- font-size: 0.9em;
- color: var(--accent2);
-}
-
-p code {
- border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent);
-}
diff --git a/assets/css/components/feed.css b/assets/css/components/feed.css
deleted file mode 100644
index 0902198..0000000
--- a/assets/css/components/feed.css
+++ /dev/null
@@ -1,101 +0,0 @@
-/* feed.css */
-.feed-section {
- padding: var(--section-py-mobile) 0;
-}
-
-@media (min-width: 768px) {
- .feed-section {
- padding: var(--section-py-desktop) 0;
- }
-}
-
-.feed-label {
- font-family: var(--font-mono);
- font-size: 0.75rem;
- letter-spacing: 0.15em;
- text-transform: uppercase;
- color: var(--accent);
- margin-bottom: 1.5rem;
-}
-
-.feed-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
- gap: var(--gap-md);
- margin-bottom: var(--gap-xl);
-}
-
-@media (min-width: 768px) {
- .feed-grid {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-@media (min-width: 1200px) {
- .feed-grid {
- grid-template-columns: repeat(3, 1fr);
- }
-}
-
-.feed-list {
- display: flex;
- flex-direction: column;
- gap: var(--gap-md);
-}
-
-.feed-list .post-card {
- flex-direction: row;
-}
-
-/* Filter bar */
-.filter-bar {
- display: flex;
- gap: var(--gap-sm);
- margin-bottom: var(--gap-lg);
- flex-wrap: wrap;
-}
-
-.filter-btn {
- font-family: var(--font-mono);
- font-size: var(--fs-badge);
- padding: 0.4rem 1rem;
- border: 1px solid var(--border);
- border-radius: 20px;
- background: transparent;
- color: var(--text-dim);
- text-transform: uppercase;
- letter-spacing: 0.1em;
- cursor: pointer;
- transition: var(--transition);
-}
-
-.filter-btn:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-
-.filter-btn.active {
- background: var(--accent);
- color: #fff;
- border-color: var(--accent);
-}
-
-/* View all link */
-.feed-cta {
- display: inline-block;
- padding: 0.75rem 1.5rem;
- background: var(--accent);
- color: #fff;
- border-radius: 4px;
- font-family: var(--font-mono);
- font-size: var(--fs-btn);
- text-transform: uppercase;
- letter-spacing: 0.1em;
- text-decoration: none;
- transition: var(--transition);
-}
-
-.feed-cta:hover {
- background: var(--accent2);
- color: var(--bg);
-}
diff --git a/assets/css/components/footer.css b/assets/css/components/footer.css
deleted file mode 100644
index 17946ec..0000000
--- a/assets/css/components/footer.css
+++ /dev/null
@@ -1,64 +0,0 @@
-/* footer.css */
-footer {
- background: var(--bg2);
- border-top: 1px solid var(--border);
- margin-top: var(--gap-xl);
- padding: 2rem 0 1rem;
- font-size: 0.85rem;
-}
-
-.footer-container {
- max-width: var(--container-max);
- margin: 0 auto;
- padding: 0 1.5rem;
- text-align: center;
- color: var(--text-dim);
-}
-
-.footer-container a {
- color: var(--accent);
-}
-
-.footer-container a:hover {
- color: var(--accent2);
-}
-
-.footer-content {
- display: flex;
- justify-content: space-between;
- align-items: center;
- flex-wrap: wrap;
- gap: 1rem;
- margin-bottom: 1rem;
-}
-
-.footer-nav {
- list-style: none;
- display: flex;
- gap: 2rem;
- margin: 0;
-}
-
-.footer-nav a {
- font-family: var(--font-mono);
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.08em;
-}
-
-.footer-copyright {
- color: var(--muted);
- font-family: var(--font-mono);
- font-size: 0.75rem;
-}
-
-@media (max-width: 600px) {
- .footer-content {
- flex-direction: column;
- }
-
- .footer-nav {
- flex-direction: column;
- gap: 0.5rem;
- }
-}
diff --git a/assets/css/components/hamburger.css b/assets/css/components/hamburger.css
deleted file mode 100644
index 2c868d8..0000000
--- a/assets/css/components/hamburger.css
+++ /dev/null
@@ -1,189 +0,0 @@
-/* hamburger.css */
-
-.nav-header {
- position: fixed;
- top: 0;
- right: 0;
- z-index: var(--z-nav);
- padding: var(--sp-6) var(--sp-8);
-}
-
-/* Hamburger Button */
-.hamburger {
- background: none;
- border: none;
- cursor: pointer;
- display: flex;
- flex-direction: column;
- gap: 0.4rem;
- padding: 0.5rem;
- transition: all var(--duration-base) ease;
- width: 32px;
- height: 32px;
- align-items: center;
- justify-content: center;
-}
-
-.hamburger span {
- display: block;
- width: 24px;
- height: 2px;
- background-color: var(--text);
- border-radius: 2px;
- transition: all var(--duration-base) ease;
- transform-origin: center;
-}
-
-.hamburger.active span:nth-child(1) {
- transform: translateY(10px) rotate(45deg);
-}
-
-.hamburger.active span:nth-child(2) {
- opacity: 0;
-}
-
-.hamburger.active span:nth-child(3) {
- transform: translateY(-10px) rotate(-45deg);
-}
-
-/* Menu Overlay */
-.menu-overlay {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background: rgba(6, 11, 16, 0.95);
- backdrop-filter: blur(4px);
- z-index: var(--z-menu);
- opacity: 0;
- visibility: hidden;
- transition: opacity var(--duration-base) ease, visibility var(--duration-base) ease;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.menu-overlay.active {
- opacity: 1;
- visibility: visible;
-}
-
-/* Menu Items */
-.menu-items {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: var(--sp-12);
- width: 90%;
- max-width: 600px;
- padding: var(--sp-8);
-}
-
-.menu-logo {
- font-family: var(--font-head);
- font-size: 1.5rem;
- font-weight: 700;
- color: var(--text);
- text-decoration: none;
- margin-bottom: var(--sp-4);
-}
-
-.menu-links {
- list-style: none;
- display: flex;
- flex-direction: column;
- gap: var(--sp-6);
- text-align: center;
-}
-
-.menu-links a {
- color: var(--text);
- text-decoration: none;
- font-family: var(--font-mono);
- font-size: 1.2rem;
- padding: var(--sp-3) var(--sp-4);
- border-radius: 4px;
- transition: all var(--duration-base) ease;
-}
-
-.menu-links a:hover {
- color: var(--accent);
-}
-
-.menu-links a[aria-current="page"] {
- color: var(--accent);
- font-weight: 700;
-}
-
-/* Menu Footer */
-.menu-footer {
- display: flex;
- justify-content: center;
- margin-top: var(--sp-8);
-}
-
-/* Theme Switch Button */
-.theme-switch {
- background: var(--surface);
- border: 1px solid var(--border);
- cursor: pointer;
- width: 50px;
- height: 26px;
- border-radius: 20px;
- padding: 2px;
- position: relative;
- transition: all var(--duration-base) ease;
- display: flex;
- align-items: center;
- justify-content: flex-start;
-}
-
-.theme-switch:hover {
- border-color: var(--accent);
-}
-
-.theme-switch-dot {
- width: 20px;
- height: 20px;
- background-color: var(--text);
- border-radius: 50%;
- display: block;
- transition: transform var(--duration-base) ease;
-}
-
-.theme-switch.light {
- justify-content: flex-end;
-}
-
-.theme-switch.light .theme-switch-dot {
- transform: translateX(24px);
-}
-
-/* Keyboard Focus */
-.hamburger:focus-visible,
-.menu-links a:focus-visible,
-.theme-switch:focus-visible {
- outline: 2px solid var(--accent);
- outline-offset: 2px;
-}
-
-/* Skip Link */
-.skip-link {
- position: fixed;
- top: 0;
- left: 0;
- background: var(--accent);
- color: var(--bg);
- padding: var(--sp-3) var(--sp-4);
- z-index: var(--z-modal);
- text-decoration: none;
- font-family: var(--font-mono);
- font-size: 0.9rem;
- transform: translateY(-100%);
- transition: transform var(--duration-base) ease;
-}
-
-.skip-link:focus {
- transform: translateY(0);
-}
diff --git a/assets/css/components/header.css b/assets/css/components/header.css
deleted file mode 100644
index acb009d..0000000
--- a/assets/css/components/header.css
+++ /dev/null
@@ -1,89 +0,0 @@
-/* header.css */
-header {
- background: var(--bg);
- border-bottom: 1px solid var(--border);
- padding: 1rem 0;
- position: sticky;
- top: 0;
- z-index: 100;
-}
-
-.header-container {
- max-width: var(--container-max);
- margin: 0 auto;
- padding: 0 1.5rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-
-.site-title {
- font-family: var(--font-head);
- font-size: 1rem;
- font-weight: 800;
- color: var(--accent);
- text-decoration: none;
- margin: 0;
-}
-
-.header-controls {
- display: flex;
- gap: 1.5rem;
- align-items: center;
-}
-
-nav ul {
- list-style: none;
- display: flex;
- gap: 2rem;
- margin: 0;
-}
-
-nav a {
- font-family: var(--font-mono);
- font-size: var(--fs-nav);
- text-transform: uppercase;
- letter-spacing: 0.08em;
- color: var(--text-dim);
- transition: var(--transition);
-}
-
-nav a:hover,
-nav a.active {
- color: var(--accent);
-}
-
-.theme-toggle {
- background: var(--surface);
- border: 1px solid var(--border);
- color: var(--text);
- padding: 0.5rem 1rem;
- border-radius: 4px;
- cursor: pointer;
- font-family: var(--font-mono);
- font-size: var(--fs-nav);
- transition: var(--transition);
-}
-
-.theme-toggle:hover {
- background: var(--border);
- color: var(--accent);
-}
-
-@media (max-width: 768px) {
- .header-container {
- flex-wrap: wrap;
- gap: 1rem;
- }
-
- nav ul {
- order: 3;
- width: 100%;
- gap: 1rem;
- flex-wrap: wrap;
- }
-
- .header-controls {
- gap: 1rem;
- }
-}
diff --git a/assets/css/components/hero.css b/assets/css/components/hero.css
deleted file mode 100644
index 53f30b3..0000000
--- a/assets/css/components/hero.css
+++ /dev/null
@@ -1,354 +0,0 @@
-/* hero.css */
-
-.hero {
- position: relative;
- height: 100vh;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- align-items: stretch;
- padding: 6rem 2rem 2rem;
- overflow: hidden;
-}
-
-.hero-container {
- position: relative;
- z-index: 2;
- display: flex;
- align-items: center;
- gap: 3rem;
- max-width: 1080px;
- width: 100%;
- margin: 0 auto;
- flex: 1;
-}
-
-#matrix-canvas {
- position: absolute;
- inset: 0;
- opacity: 0.13;
- pointer-events: none;
- z-index: 1;
-}
-
-html.theme-light #matrix-canvas {
- opacity: 0.08;
-}
-
-html.theme-light #matrix-canvas {
- opacity: 0.08;
-}
-
-.hero-left {
- flex: 1;
- min-width: 0;
-}
-
-.hero-prompt {
- font-size: 0.75rem;
- letter-spacing: 0.16em;
- text-transform: uppercase;
- color: var(--accent);
- margin-bottom: 0.5rem;
- font-weight: 500;
- font-family: var(--font-mono);
-}
-
-.hero-name {
- font-family: var(--font-head);
- font-size: clamp(3rem, 12vw, 7rem);
- font-weight: 800;
- letter-spacing: -0.04em;
- line-height: 1;
- margin-bottom: 1rem;
- position: relative;
- display: inline-block;
-}
-
-/* Glitch effect on hero name */
-.hero-name::before,
-.hero-name::after {
- content: attr(data-text);
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- opacity: 0;
- pointer-events: none;
- overflow: hidden;
- font-family: inherit;
- font-size: inherit;
- font-weight: inherit;
- letter-spacing: inherit;
- line-height: inherit;
-}
-
-.hero-name::before {
- color: #ff2b6d;
-}
-
-.hero-name::after {
- color: #00e5ff;
-}
-
-.hero-name.is-glitching::before {
- opacity: 0.8;
- animation: glitch-red 0.45s steps(3) forwards;
-}
-
-.hero-name.is-glitching::after {
- opacity: 0.8;
- animation: glitch-cyn 0.45s steps(3) forwards;
-}
-
-@keyframes glitch-red {
- 0% { clip-path: inset(0 0 0 0); transform: translate(0); }
- 20% { clip-path: inset(0 0 65% 0); transform: translate(-0.05em, -0.03em); }
- 40% { clip-path: inset(28% 0 58% 0); transform: translate(0.05em, 0.03em); }
- 60% { clip-path: inset(44% 0 58% 0); transform: translate(-0.05em, -0.02em); }
- 80% { clip-path: inset(12% 0 85% 0); transform: translate(0.05em, 0.02em); }
- 100% { clip-path: inset(0 0 0 0); transform: translate(0); }
-}
-
-@keyframes glitch-cyn {
- 0% { clip-path: inset(0 0 0 0); transform: translate(0); }
- 20% { clip-path: inset(0 0 60% 0); transform: translate(0.05em, 0.02em); }
- 40% { clip-path: inset(38% 0 58% 0); transform: translate(-0.05em, 0.01em); }
- 60% { clip-path: inset(19% 0 40% 0); transform: translate(0.025em, -0.02em); }
- 80% { clip-path: inset(1% 0 58% 0); transform: translate(-0.05em, -0.02em); }
- 100% { clip-path: inset(0 0 0 0); transform: translate(0); }
-}
-
-.hero-role {
- font-size: clamp(0.85rem, 3vw, 1rem);
- letter-spacing: 0.05em;
- margin-bottom: 1.5rem;
- color: var(--accent);
- font-weight: 400;
- min-height: 1.5em;
- font-family: var(--font-mono);
-}
-
-.cursor {
- display: inline-block;
- width: 0.15em;
- height: 1em;
- background: var(--accent);
- margin-left: 0.1em;
- animation: cursor-blink 1s step-end infinite;
-}
-
-@keyframes cursor-blink {
- 0%, 49% { opacity: 1; }
- 50%, 100% { opacity: 0; }
-}
-
-.hero-tagline {
- font-family: var(--font-body);
- font-size: 1rem;
- font-weight: 400;
- line-height: 1.95;
- color: var(--text-dim);
- margin-bottom: 2rem;
- max-width: 90%;
-}
-
-/* Buttons */
-.hero-buttons {
- display: flex;
- gap: 1rem;
- flex-wrap: wrap;
- margin-bottom: 2rem;
-}
-
-.btn {
- padding: 0.75rem 1.5rem;
- font-size: 0.8rem;
- font-family: var(--font-mono);
- letter-spacing: 0.1em;
- text-transform: uppercase;
- font-weight: 500;
- cursor: pointer;
- border: none;
- transition: all var(--duration-base) ease;
- text-decoration: none;
- display: inline-block;
- border-radius: 0;
-}
-
-.btn-primary {
- background: var(--accent);
- color: #000;
- font-weight: 600;
-}
-
-.btn-primary:hover {
- box-shadow: 0 0 24px rgba(168, 85, 247, 0.45);
- transform: translate(0, -2px);
-}
-
-.btn-outline {
- background: transparent;
- color: var(--accent);
- border: 1px solid var(--border);
-}
-
-.btn-outline:hover {
- background: var(--accent);
- color: #000;
- box-shadow: 0 0 24px rgba(168, 85, 247, 0.45);
-}
-
-/* Right column: terminal widget */
-.hero-right {
- flex: 0 0 auto;
- width: 320px;
- display: none;
-}
-
-@media (min-width: 900px) {
- .hero-right {
- display: block;
- }
-}
-
-.hero-terminal {
- background: rgba(6, 11, 16, 0.85);
- border: 1px solid rgba(168, 85, 247, 0.3);
- border-radius: 8px;
- overflow: hidden;
- font-size: 0.75rem;
- line-height: 1.7;
-}
-
-.terminal-bar {
- background: var(--surface);
- padding: 0.5rem 1rem;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.terminal-dot {
- width: 12px;
- height: 12px;
- border-radius: 50%;
- display: inline-block;
-}
-
-.terminal-title {
- margin-left: auto;
- font-family: var(--font-mono);
- font-size: 0.7rem;
- color: var(--text-dim);
-}
-
-.terminal-content {
- padding: 1rem;
- font-family: var(--font-mono);
- color: var(--terminal-text);
- font-size: 0.75rem;
- line-height: 1.7;
-}
-
-.terminal-content .tl {
- white-space: nowrap;
- word-wrap: break-word;
-}
-
-/* Terminal color classes */
-.tc-dim { color: var(--muted); }
-.tc-ok { color: var(--accent2); }
-.tc-key { color: var(--accent); }
-
-/* Terminal stagger animation */
-@keyframes terminal-fade-in {
- from {
- opacity: 0;
- transform: translateY(4px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-
-.tl {
- opacity: 0;
- animation: terminal-fade-in 0.4s ease forwards;
-}
-
-.tl-d1 { animation-delay: 0ms; }
-.tl-d2 { animation-delay: 150ms; }
-.tl-d3 { animation-delay: 300ms; }
-.tl-d4 { animation-delay: 450ms; }
-.tl-d5 { animation-delay: 600ms; }
-.tl-d6 { animation-delay: 750ms; }
-.tl-d7 { animation-delay: 900ms; }
-.tl-d8 { animation-delay: 1050ms; }
-.tl-d9 { animation-delay: 1200ms; }
-
-.terminal-prompt {
- color: var(--terminal-prompt);
-}
-
-/* Scroll Indicator */
-.scroll-indicator {
- position: absolute;
- bottom: 2rem;
- left: 50%;
- transform: translateX(-50%);
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.5rem;
- font-family: var(--font-mono);
- font-size: 0.7rem;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: var(--text-dim);
- z-index: 2;
-}
-
-.scroll-line {
- width: 1px;
- height: 40px;
- background: var(--accent);
- animation: scroll-line-pulse 2s ease-in-out infinite;
-}
-
-@keyframes scroll-line-pulse {
- 0%, 100% { opacity: 0.3; transform: scaleY(1); }
- 50% { opacity: 1; transform: scaleY(1.15); }
-}
-
-/* Mobile */
-@media (max-width: 900px) {
- .hero {
- height: auto;
- min-height: 100vh;
- padding-top: 6rem;
- }
-
- .hero-container {
- flex-direction: column;
- }
-
- .hero-tagline {
- max-width: 100%;
- }
-
- .scroll-indicator {
- display: none;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .hero-name.is-glitching::before,
- .hero-name.is-glitching::after { animation: none; }
- .cursor { animation: none; }
- .scroll-line { animation: none; opacity: 0.6; }
- .tl { animation: none; opacity: 1; }
-}
diff --git a/assets/css/components/lightbox.css b/assets/css/components/lightbox.css
deleted file mode 100644
index ad34e84..0000000
--- a/assets/css/components/lightbox.css
+++ /dev/null
@@ -1,170 +0,0 @@
-/* lightbox.css */
-
-.photo-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
- gap: 1.5rem;
- margin: 2rem 0;
-}
-
-.photo-card {
- position: relative;
- cursor: pointer;
- overflow: hidden;
- border-radius: 4px;
- border: 1px solid var(--border);
- transition: all var(--duration-base) ease;
-}
-
-.photo-card:hover {
- border-color: var(--accent);
- box-shadow: 0 0 20px rgba(168, 85, 247, 0.2);
- transform: translateY(-4px);
-}
-
-.photo-card img {
- width: 100%;
- height: 200px;
- object-fit: cover;
- display: block;
- transition: transform var(--duration-base) ease;
-}
-
-.photo-card:hover img {
- transform: scale(1.05);
-}
-
-.photo-card figcaption {
- padding: 1rem;
- background: var(--surface);
- font-size: 0.9rem;
- color: var(--text-dim);
-}
-
-/* Lightbox Modal */
-.photo-lightbox {
- position: fixed;
- inset: 0;
- z-index: 200;
- display: none;
- background: rgba(6, 11, 16, 0.95);
- backdrop-filter: blur(4px);
-}
-
-.photo-lightbox.active {
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.photo-lightbox-backdrop {
- position: absolute;
- inset: 0;
- cursor: pointer;
-}
-
-.photo-lightbox-content {
- position: relative;
- z-index: 201;
- max-width: 90vw;
- max-height: 90vh;
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.photo-lightbox-image {
- max-width: 100%;
- max-height: 70vh;
- object-fit: contain;
-}
-
-.photo-lightbox-controls {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 1rem;
- background: var(--surface);
- border-radius: 4px;
-}
-
-.photo-lightbox-nav {
- display: flex;
- gap: 1rem;
-}
-
-.photo-lightbox-nav button {
- padding: 0.5rem 1rem;
- background: var(--accent);
- color: #000;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-weight: 600;
- transition: all var(--duration-base) ease;
-}
-
-.photo-lightbox-nav button:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-.photo-lightbox-nav button:not(:disabled):hover {
- box-shadow: 0 0 20px rgba(168, 85, 247, 0.4);
- transform: translateY(-2px);
-}
-
-.photo-lightbox-close {
- position: absolute;
- top: 1rem;
- right: 1rem;
- background: var(--surface);
- border: 1px solid var(--border);
- width: 44px;
- height: 44px;
- border-radius: 50%;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 1.5rem;
- color: var(--text);
- transition: all var(--duration-base) ease;
- z-index: 202;
-}
-
-.photo-lightbox-close:hover {
- background: var(--accent);
- color: #000;
- border-color: var(--accent);
-}
-
-.photo-metadata {
- padding: 1rem;
- background: var(--surface);
- border-radius: 4px;
- font-size: 0.9rem;
- color: var(--text-dim);
-}
-
-.photo-metadata dt {
- font-weight: 600;
- color: var(--text);
- margin-top: 0.5rem;
-}
-
-.photo-metadata dt:first-child {
- margin-top: 0;
-}
-
-.photo-metadata dd {
- margin-left: 1rem;
- font-family: var(--font-mono);
-}
-
-@media (prefers-reduced-motion: reduce) {
- .photo-card:hover img,
- .photo-lightbox-nav button:hover {
- transform: none;
- }
-}
diff --git a/assets/css/components/progress-bar.css b/assets/css/components/progress-bar.css
deleted file mode 100644
index ee17fcc..0000000
--- a/assets/css/components/progress-bar.css
+++ /dev/null
@@ -1,19 +0,0 @@
-/* progress-bar.css */
-
-.progress-bar {
- position: fixed;
- top: 0;
- left: 0;
- height: 2px;
- background: linear-gradient(to right, var(--accent), var(--accent2));
- box-shadow: 0 0 10px var(--accent-glow);
- z-index: var(--z-progress);
- width: 0%;
- transition: width var(--duration-base) ease;
-}
-
-@media (prefers-reduced-motion: reduce) {
- .progress-bar {
- transition: none;
- }
-}
diff --git a/assets/css/components/share-sidebar.css b/assets/css/components/share-sidebar.css
deleted file mode 100644
index 8bc8d1d..0000000
--- a/assets/css/components/share-sidebar.css
+++ /dev/null
@@ -1,107 +0,0 @@
-/* share-sidebar.css */
-
-.share-sidebar {
- position: fixed;
- right: 2rem;
- bottom: 50%;
- transform: translateY(50%);
- z-index: 30;
- display: none;
-}
-
-@media (min-width: 1200px) {
- .share-sidebar {
- display: block;
- }
-}
-
-.share-buttons {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.share-btn {
- width: 44px;
- height: 44px;
- border-radius: 50%;
- background: var(--surface);
- border: 1px solid var(--border);
- color: var(--text);
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all var(--duration-base) ease;
- position: relative;
-}
-
-.share-btn:hover {
- background: var(--accent);
- border-color: var(--accent);
- color: #000;
- transform: scale(1.1);
-}
-
-.share-btn svg {
- width: 20px;
- height: 20px;
-}
-
-/* Tooltip */
-.share-btn::after {
- content: attr(data-label);
- position: absolute;
- right: 100%;
- top: 50%;
- transform: translateY(-50%);
- margin-right: 0.75rem;
- background: var(--surface);
- border: 1px solid var(--border);
- padding: 0.4rem 0.8rem;
- border-radius: 4px;
- white-space: nowrap;
- font-size: 0.75rem;
- color: var(--text-dim);
- opacity: 0;
- visibility: hidden;
- transition: all var(--duration-base) ease;
- pointer-events: none;
-}
-
-.share-btn:hover::after {
- opacity: 1;
- visibility: visible;
- margin-right: 1rem;
-}
-
-/* Mobile: horizontal strip below article */
-@media (max-width: 1199px) {
- .share-sidebar {
- position: static;
- transform: none;
- display: flex;
- justify-content: center;
- margin: 2rem 0;
- padding: 2rem 1.5rem;
- border-top: 1px solid var(--border);
- border-bottom: 1px solid var(--border);
- }
-
- .share-buttons {
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: center;
- }
-}
-
-@media (prefers-reduced-motion: reduce) {
- .share-btn,
- .share-btn::after {
- transition: none;
- }
-
- .share-btn:hover {
- transform: none;
- }
-}
diff --git a/assets/css/components/timeline.css b/assets/css/components/timeline.css
deleted file mode 100644
index c4a3678..0000000
--- a/assets/css/components/timeline.css
+++ /dev/null
@@ -1,336 +0,0 @@
-/* timeline.css */
-
-.page-header {
- padding: 4rem 2rem 2rem;
- max-width: var(--container-max);
- margin: 0 auto;
- text-align: center;
-}
-
-.page-header h1 {
- margin-bottom: 2rem;
-}
-
-.filter-buttons {
- display: flex;
- justify-content: center;
- gap: 1rem;
- flex-wrap: wrap;
-}
-
-.filter-btn {
- padding: 0.6rem 1.2rem;
- font-family: var(--font-mono);
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.1em;
- background: transparent;
- border: 1px solid var(--border);
- color: var(--text);
- cursor: pointer;
- border-radius: 4px;
- transition: all var(--duration-base) ease;
-}
-
-.filter-btn:hover {
- border-color: var(--accent);
- color: var(--accent);
-}
-
-.filter-btn.active {
- background: var(--accent);
- color: #000;
- border-color: var(--accent);
- font-weight: 600;
-}
-
-/* Featured Article */
-.featured-article {
- max-width: var(--container-max);
- margin: 3rem auto;
- padding: 2rem;
- background: var(--surface);
- border: 2px solid transparent;
- border-image: linear-gradient(135deg, var(--accent), var(--accent2)) 1;
- border-radius: 8px;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 2rem;
- align-items: center;
-}
-
-@media (max-width: 768px) {
- .featured-article {
- grid-template-columns: 1fr;
- }
-}
-
-.featured-image {
- width: 100%;
- overflow: hidden;
- border-radius: 4px;
-}
-
-.featured-image img {
- width: 100%;
- height: 300px;
- object-fit: cover;
-}
-
-.featured-body {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.featured-header {
- display: flex;
- gap: 1rem;
- align-items: center;
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.1em;
-}
-
-.featured-type-badge {
- font-weight: 600;
-}
-
-.featured-date {
- color: var(--text-dim);
-}
-
-.featured-title {
- font-size: clamp(1.5rem, 4vw, 2.5rem);
- line-height: 1.2;
- margin-bottom: 1rem;
-}
-
-.featured-excerpt {
- color: var(--text-dim);
- line-height: 1.8;
- margin-bottom: 1rem;
-}
-
-.featured-link {
- color: var(--accent);
- text-decoration: none;
- font-family: var(--font-mono);
- font-size: 0.9rem;
- transition: all var(--duration-base) ease;
-}
-
-.featured-link:hover {
- color: var(--accent2);
- transform: translateX(4px);
-}
-
-/* Timeline */
-.timeline-section {
- position: relative;
- max-width: var(--container-max);
- margin: 4rem auto;
- padding: 0 2rem;
-}
-
-.timeline-line {
- position: absolute;
- left: 50%;
- top: 0;
- bottom: 0;
- width: 2px;
- background: linear-gradient(to bottom, var(--accent), transparent);
- transform: translateX(-50%);
-}
-
-@media (max-width: 900px) {
- .timeline-line {
- left: 0;
- }
-}
-
-.timeline-feed {
- position: relative;
- padding: 2rem 0;
-}
-
-.timeline-item {
- position: relative;
- margin-bottom: 3rem;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 2rem;
- align-items: center;
-}
-
-.timeline-item.left {
- grid-template-columns: 1fr 1fr;
-}
-
-.timeline-item.right {
- grid-template-columns: 1fr 1fr;
- direction: rtl;
-}
-
-.timeline-item.right > * {
- direction: ltr;
-}
-
-@media (max-width: 900px) {
- .timeline-item,
- .timeline-item.left,
- .timeline-item.right {
- grid-template-columns: 1fr;
- direction: ltr;
- }
-
- .timeline-item.right > * {
- direction: ltr;
- }
-}
-
-.timeline-dot {
- position: absolute;
- left: 50%;
- top: 2rem;
- width: 16px;
- height: 16px;
- background: var(--accent);
- border: 3px solid var(--bg);
- border-radius: 50%;
- transform: translateX(-50%);
- z-index: 2;
-}
-
-@media (max-width: 900px) {
- .timeline-dot {
- left: -8px;
- }
-}
-
-.article-card {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: 8px;
- overflow: hidden;
- transition: all var(--duration-base) ease;
- cursor: pointer;
-}
-
-.article-card:hover {
- border-color: var(--accent);
- box-shadow: 0 0 20px rgba(168, 85, 247, 0.2);
- transform: translateY(-4px);
-}
-
-.article-card-image {
- width: 100%;
- height: 200px;
- overflow: hidden;
-}
-
-.article-card-image img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- transition: transform var(--duration-base) ease;
-}
-
-.article-card:hover .article-card-image img {
- transform: scale(1.05);
-}
-
-.article-card-body {
- padding: 1.5rem;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.article-card-header {
- display: flex;
- gap: 1rem;
- align-items: center;
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.1em;
-}
-
-.article-type-badge {
- font-weight: 600;
- padding: 0.3rem 0.6rem;
- border-radius: 4px;
- background: color-mix(in srgb, currentColor 15%, transparent);
- border: 1px solid color-mix(in srgb, currentColor 30%, transparent);
-}
-
-.article-type-badge.type-tech {
- color: var(--type-tech);
-}
-
-.article-type-badge.type-life {
- color: var(--type-life);
-}
-
-.article-type-badge.type-quote {
- color: var(--type-quote);
-}
-
-.article-type-badge.type-link {
- color: var(--type-link);
-}
-
-.article-type-badge.type-photo {
- color: var(--type-photo);
-}
-
-.article-date {
- color: var(--text-dim);
-}
-
-.article-card-title {
- font-size: 1.3rem;
- font-weight: 700;
- line-height: 1.3;
- margin: 0.5rem 0;
-}
-
-.article-card-title a {
- color: var(--text);
- text-decoration: none;
- transition: color var(--duration-base) ease;
-}
-
-.article-card-title a:hover {
- color: var(--accent);
-}
-
-.article-card-excerpt {
- color: var(--text-dim);
- font-size: 0.95rem;
- line-height: 1.6;
- display: -webkit-box;
- -webkit-line-clamp: 2;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-
-/* Hide/show articles based on filter */
-.timeline-item[data-type] {
- display: none;
-}
-
-.timeline-item[data-type].visible,
-.timeline-item[data-filter="all"].visible {
- display: grid;
-}
-
-@media (prefers-reduced-motion: reduce) {
- .article-card:hover {
- transform: none;
- }
-
- .article-card-image img {
- transition: none;
- }
-}
diff --git a/assets/css/main.css b/assets/css/main.css
index 44b4cd1..ab41a7d 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -1,203 +1,1709 @@
-@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;600&family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,700;1,300&family=Oxanium:wght@700;800&display=swap');
-
-@import 'variables.css';
-@import 'components/hamburger.css';
-@import 'components/header.css';
-@import 'components/footer.css';
-@import 'components/hero.css';
-@import 'components/card.css';
-@import 'components/feed.css';
-@import 'components/timeline.css';
-@import 'components/article-hero.css';
-@import 'components/share-sidebar.css';
-@import 'components/lightbox.css';
-@import 'components/404.css';
-@import 'components/code.css';
-@import 'components/progress-bar.css';
-
-/* Base Styles */
-html {
- font-size: 17px;
- scroll-behavior: smooth;
-}
-
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- background-color: var(--bg);
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* Dark theme (default) - CSS custom properties */
+:root {
+ --bg: #060b10;
+ --bg2: #0c1520;
+ --bg2-rgb: 12, 21, 32;
+ --surface: #101e2d;
+ --surface-rgb: 16, 30, 45;
+ --border: #182840;
+ --accent: #a855f7;
+ --accent-rgb: 168, 85, 247;
+ --accent2: #00ff88;
+ --accent-glow: rgba(168, 85, 247, 0.12);
+ --text: #c4d6e8;
+ --text-dim: #7a9bb8;
+ --muted: #304860;
+ /* Article type colors - dark */
+ --type-tech: #a855f7;
+ --type-life: #f59e0b;
+ --type-quote: #00ff88;
+ --type-link: #38bdf8;
+ --type-photo: #ec4899;
+ /* Article type text colors - dark (all black for WCAG AA) */
+ --type-tech-text: #000000;
+ --type-life-text: #000000;
+ --type-quote-text: #000000;
+ --type-link-text: #000000;
+ --type-photo-text: #000000;
+}
+
+/* Light theme overrides */
+html.theme-light {
+ --bg: #ffffff;
+ --bg2: #f8f9fa;
+ --bg2-rgb: 248, 249, 250;
+ --surface: #f0f3f7;
+ --surface-rgb: 240, 243, 247;
+ --border: #d9dfe8;
+ --accent: #9333ea;
+ --accent-rgb: 147, 51, 234;
+ --accent2: #10b981;
+ --accent-glow: rgba(147, 51, 234, 0.1);
+ --text: #1f2937;
+ --text-dim: #374151;
+ --muted: #d1d5db;
+ /* Article type colors - light */
+ --type-tech: #7c3aed;
+ --type-life: #d97706;
+ --type-quote: #008f5a;
+ --type-link: #0284c7;
+ --type-photo: #be185d;
+ /* Article type text colors - light (mixed for WCAG AA) */
+ --type-tech-text: #ffffff;
+ --type-life-text: #000000;
+ --type-quote-text: #000000;
+ --type-link-text: #000000;
+ --type-photo-text: #ffffff;
+}
+
+/* No-JS fallback: prefers-color-scheme light */
+@media (prefers-color-scheme: light) {
+ html:not(.theme-dark) {
+ --bg: #ffffff;
+ --bg2: #f8f9fa;
+ --bg2-rgb: 248, 249, 250;
+ --surface: #f0f3f7;
+ --surface-rgb: 240, 243, 247;
+ --border: #d9dfe8;
+ --accent: #9333ea;
+ --accent-rgb: 147, 51, 234;
+ --accent2: #10b981;
+ --accent-glow: rgba(147, 51, 234, 0.1);
+ --text: #1f2937;
+ --text-dim: #374151;
+ --muted: #d1d5db;
+ --type-tech: #7c3aed;
+ --type-life: #d97706;
+ --type-quote: #008f5a;
+ --type-link: #0284c7;
+ --type-photo: #be185d;
+ --type-tech-text: #ffffff;
+ --type-life-text: #000000;
+ --type-quote-text: #000000;
+ --type-link-text: #000000;
+ --type-photo-text: #ffffff;
+ }
+}
+
+/* Theme-aware picture element for default thumbnails */
+html.theme-light picture img[src="/images/default_thumbnail_dark.png"] {
+ content: url('/images/default_thumbnail_light.png');
+}
+
+@layer base {
+ html {
+ @apply overflow-x-hidden;
+ }
+
+ body {
+ @apply bg-bg text-text font-body overflow-x-hidden;
+ }
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ @apply font-bold;
+ font-family: 'Oxanium', monospace;
+ }
+
+ h1 {
+ @apply text-3xl md:text-4xl;
+ }
+
+ h2 {
+ @apply text-2xl md:text-3xl;
+ }
+
+ h3 {
+ @apply text-xl md:text-2xl;
+ }
+
+ .heading-prefix {
+ font-family: 'JetBrains Mono', monospace;
+ font-weight: 400;
+ font-size: 0.8em;
+ color: var(--accent);
+ opacity: 0.7;
+ margin-right: 0.35em;
+ user-select: none;
+ }
+
+ a {
+ @apply text-accent hover:opacity-80 transition-opacity;
+ }
+
+ code {
+ @apply font-mono bg-surface border border-border px-1.5 py-0.5 rounded text-accent2;
+ }
+
+ pre {
+ background-color: rgba(var(--surface-rgb), 0.8);
+ @apply p-4 rounded border border-border overflow-x-auto;
+ }
+
+ pre code {
+ @apply bg-transparent border-0 p-0 text-text;
+ }
+
+ *:focus-visible {
+ @apply ring-2 ring-accent ring-offset-2;
+ ring-offset-color: var(--bg);
+ }
+
+ button,
+ input,
+ textarea,
+ select {
+ @apply transition-colors duration-200;
+ }
+}
+
+@layer components {
+ .container {
+ @apply max-w-4xl mx-auto;
+ }
+
+ /* Background utilities */
+ .bg-bg {
+ background-color: var(--bg);
+ }
+
+ .bg-bg2 {
+ background-color: var(--bg2);
+ }
+
+ .bg-surface {
+ background-color: var(--surface);
+ }
+
+ /* Border utilities */
+ .border-border {
+ border-color: var(--border);
+ }
+
+ /* Text color utilities */
+ .text-accent {
+ color: var(--accent);
+ }
+
+ .text-accent2 {
+ color: var(--accent2);
+ }
+
+ .text-text {
+ color: var(--text);
+ }
+
+ .text-text-dim {
+ color: var(--text-dim);
+ }
+
+ /* Additional semantic utilities */
+ .text-muted {
+ color: var(--muted);
+ }
+
+ .bg-muted {
+ background-color: var(--muted);
+ }
+
+ /* Glow effect utility */
+ .glow-accent {
+ box-shadow: 0 0 20px var(--accent-glow);
+ }
+
+ /* Frosted glass bar (header/footer) */
+ .frosted-bar {
+ background-color: rgba(var(--bg2-rgb), 0.75);
+ backdrop-filter: blur(10px);
+ box-shadow: 0 0 20px var(--accent-glow);
+ /* border applied via utility classes in templates */
+ }
+
+ /* Border utilities for frosted-bar component */
+ .frosted-bar.border-b,
+ .frosted-bar.border-t {
+ border-color: var(--border);
+ }
+
+ /* Button component styles */
+ .btn {
+ @apply inline-flex items-center justify-center px-4 py-2 rounded font-bold transition-all duration-200 cursor-pointer;
+ background-color: var(--accent);
+ color: #ffffff;
+ border: none;
+ outline: none;
+ }
+
+ .btn:hover:not(:disabled) {
+ opacity: 0.85;
+ transform: translateY(-1px);
+ }
+
+ .btn:focus-visible {
+ @apply ring-2 ring-offset-2;
+ ring-color: var(--accent);
+ ring-offset-color: var(--bg);
+ }
+
+ .btn:active:not(:disabled) {
+ transform: translateY(0);
+ opacity: 0.75;
+ }
+
+ .btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+
+ /* Button variants */
+ .btn-primary {
+ background-color: var(--accent);
+ color: #ffffff;
+ }
+
+ .btn-primary:hover:not(:disabled) {
+ background-color: var(--accent);
+ }
+
+ .btn-secondary {
+ background-color: var(--accent2);
+ color: var(--bg);
+ font-weight: 600;
+ }
+
+ .btn-secondary:hover:not(:disabled) {
+ background-color: var(--accent2);
+ }
+
+ .btn-outline {
+ background-color: transparent;
+ color: var(--accent);
+ border: 2px solid var(--accent);
+ }
+
+ .btn-outline:hover:not(:disabled) {
+ background-color: var(--accent);
+ color: #ffffff;
+ }
+
+ /* Button sizes */
+ .btn-sm {
+ @apply px-3 py-1 text-sm;
+ }
+
+ .btn-lg {
+ @apply px-6 py-3 text-lg;
+ }
+
+ /* Icon button (for icons without text) */
+ .btn-icon {
+ @apply rounded-full inline-flex items-center justify-center;
+ width: auto;
+ height: auto;
+ padding: 0.5rem;
+ }
+
+ .btn-icon svg,
+ .btn-icon i {
+ width: auto !important;
+ height: auto !important;
+ }
+
+ /* Force Feather icons to match size */
+ .btn-icon [data-feather] {
+ width: 50px !important;
+ height: 50px !important;
+ }
+
+ .btn-icon [data-feather] svg {
+ width: 50px !important;
+ height: 50px !important;
+ }
+
+ /* Sidebar widget — no box, no border */
+ .sidebar-widget {
+ margin-bottom: 1.5rem;
+ }
+
+ /* Sidebar widget title — bash comment style */
+ .sidebar-widget-label {
+ font-family: var(--font-mono, monospace);
+ font-size: 1rem;
+ font-weight: bold;
+ color: var(--accent);
+ letter-spacing: 0.08em;
+ margin-bottom: 0.5rem;
+ }
+
+ /* Sidebar separator */
+ .sidebar-hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin-bottom: 1.5rem;
+ }
+
+ /* =====================
+ Tag Cloud Component
+ ===================== */
+
+ .tag-cloud {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.75rem;
+ align-items: baseline;
+ overflow: visible;
+ }
+
+ .tag-cloud-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0.25rem 0.625rem;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
+ color: var(--text-dim);
+ text-decoration: none;
+ background-color: var(--bg2);
+ transition: border-color 150ms ease-out, color 150ms ease-out, background-color 150ms ease-out, opacity 150ms ease-out;
+ white-space: nowrap;
+ line-height: 1.4;
+ }
+
+ .tag-cloud-link:hover {
+ border-color: rgba(var(--accent-rgb), 0.5);
+ color: var(--accent);
+ background-color: rgba(var(--accent-rgb), 0.1);
+ }
+
+ .tag-cloud-link:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ border-radius: 0.25rem;
+ }
+
+ .tag-cloud-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0.375rem;
+ border-radius: 9999px;
+ font-size: 0.65em;
+ font-weight: 600;
+ background-color: rgba(var(--accent-rgb), 0.12);
+ color: var(--accent);
+ line-height: 1.6;
+ min-width: 1.2em;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .tag-cloud-link {
+ transition: none;
+ }
+ }
+
+ .share-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 50px);
+ justify-content: space-evenly;
+ justify-items: center;
+ align-content: space-evenly;
+ align-items: center;
+ }
+
+ .btn-share {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 50px;
+ height: 50px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--text-dim);
+ cursor: pointer;
+ transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
+ text-decoration: none;
+ }
+
+ .btn-share svg,
+ .btn-share i,
+ .btn-share [data-feather] {
+ width: 22px !important;
+ height: 22px !important;
+ flex-shrink: 0;
+ }
+
+ .btn-share:hover {
+ color: var(--accent);
+ border-color: rgba(var(--accent-rgb), 0.5);
+ background: rgba(var(--accent-rgb), 0.06);
+ box-shadow: 0 0 10px var(--accent-glow);
+ }
+
+ .btn-share:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ color: var(--accent);
+ border-color: var(--accent);
+ }
+
+ .btn-share--copied {
+ color: var(--accent2);
+ border-color: var(--accent2);
+ background: var(--surface);
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ .btn-share {
+ transition: none;
+ }
+ }
+
+ /* Badge base style */
+ .badge {
+ @apply inline-flex items-center px-2.5 py-1 rounded text-sm font-mono font-semibold whitespace-nowrap transition-all duration-200;
+ border: 1px solid;
+ }
+
+ /* Article type badge styles */
+ .badge-tech {
+ color: var(--type-tech);
+ background-color: rgba(168, 85, 247, 0.1);
+ border-color: rgba(168, 85, 247, 0.3);
+ }
+
+ .badge-tech:hover {
+ background-color: rgba(168, 85, 247, 0.2);
+ }
+
+ .badge-life {
+ color: var(--type-life);
+ background-color: rgba(245, 158, 11, 0.1);
+ border-color: rgba(245, 158, 11, 0.3);
+ }
+
+ .badge-life:hover {
+ background-color: rgba(245, 158, 11, 0.2);
+ }
+
+ .badge-quote {
+ color: var(--type-quote);
+ background-color: rgba(0, 255, 136, 0.1);
+ border-color: rgba(0, 255, 136, 0.3);
+ }
+
+ .badge-quote:hover {
+ background-color: rgba(0, 255, 136, 0.2);
+ }
+
+ .badge-link {
+ color: var(--type-link);
+ background-color: rgba(56, 189, 248, 0.1);
+ border-color: rgba(56, 189, 248, 0.3);
+ }
+
+ .badge-link:hover {
+ background-color: rgba(56, 189, 248, 0.2);
+ }
+
+ .badge-photo {
+ color: var(--type-photo);
+ background-color: rgba(236, 72, 153, 0.1);
+ border-color: rgba(236, 72, 153, 0.3);
+ }
+
+ .badge-photo:hover {
+ background-color: rgba(236, 72, 153, 0.2);
+ }
+
+ /* Legacy type-* classes for compatibility (with badge styling) */
+ .type-tech {
+ color: var(--type-tech);
+ background-color: rgba(168, 85, 247, 0.1);
+ }
+
+ .type-life {
+ color: var(--type-life);
+ background-color: rgba(245, 158, 11, 0.1);
+ }
+
+ .type-quote {
+ color: var(--type-quote);
+ background-color: rgba(0, 255, 136, 0.1);
+ }
+
+ .type-link {
+ color: var(--type-link);
+ background-color: rgba(56, 189, 248, 0.1);
+ }
+
+ .type-photo {
+ color: var(--type-photo);
+ background-color: rgba(236, 72, 153, 0.1);
+ }
+
+ /* Card component */
+ .card {
+ @apply border border-border rounded-lg overflow-hidden transition-all duration-200;
+ box-shadow: 0 0 20px var(--accent-glow);
+ }
+
+ .card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0 30px var(--accent-glow);
+ }
+
+ .card-image {
+ @apply aspect-video object-cover w-full;
+ }
+
+ .card-body {
+ @apply p-5 md:p-6 space-y-3;
+ }
+
+ .card-title {
+ @apply text-xl font-semibold;
+ }
+
+ .card-excerpt {
+ @apply text-text-dim text-sm line-clamp-3;
+ }
+
+ .card-footer {
+ @apply flex items-center justify-between gap-4;
+ }
+
+ /* =====================
+ Timeline Layout
+ ===================== */
+
+ .timeline {
+ @apply relative mx-auto max-w-5xl px-4 py-12;
+ }
+
+ /* Vertical spine */
+ .timeline::before {
+ content: '';
+ @apply absolute top-0 bottom-0;
+ /* Mobile: fixed 20px from container left edge */
+ left: 20px;
+ width: 2px;
+ background: linear-gradient(to bottom, var(--accent), var(--accent2, var(--accent)));
+ opacity: 0.7;
+ }
+
+ @screen md {
+ .timeline::before {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+ }
+
+ /* Each timeline row — block, full width, positioned context for abs children */
+ .timeline-item {
+ @apply relative mb-10;
+ /*
+ Mobile geometry (.timeline has px-4 = 16px padding):
+ spine left = 20px from <ol> border → 20-16 = 4px from item's containing block.
+ We want card to start ~8px right of spine right edge (22px from <ol> = 6px from block).
+ Use margin-left: 30px so card starts at 16+30=46px from <ol>, 24px right of spine right.
+ abs children: spine left relative to item = 4 - 30 = -26px.
+ */
+ margin-left: 30px;
+ }
+
+ @screen md {
+ .timeline-item {
+ margin-left: 0;
+ }
+ }
+
+ /* ---- Connector line ---- */
+ .timeline-connector {
+ @apply absolute;
+ top: 20px;
+ height: 2px;
+ /*
+ Mobile: .timeline px-4(16px) + item margin-left(30px) = 46px from <ol> border.
+ Spine right edge = 22px from <ol> border = 22-46 = -24px from item left.
+ Connector: left=-24px, width=24px → touches spine right and card left edge.
+ */
+ left: -24px;
+ width: 24px;
+ }
+
+ @screen md {
+ /*
+ spine center = 50% of item. Spine is 2px: left edge at 50%-1px, right at 50%+1px.
+ Cards occupy [0 .. 50%-24px] (left) or [50%+24px .. 100%] (right).
+ Left connector: left=50%-24px, width=23px → touches card right edge and spine left.
+ Right connector: left=50%+1px, width=23px → touches spine right and card left.
+ */
+ .timeline-item--left .timeline-connector {
+ left: calc(50% - 24px);
+ width: 23px;
+ }
+ .timeline-item--right .timeline-connector {
+ left: calc(50% + 1px);
+ width: 23px;
+ }
+ }
+
+ /* ---- Node on spine ---- */
+ .timeline-node {
+ @apply absolute rounded-full z-10;
+ top: 14px;
+ /*
+ Mobile: spine center = 21px from <ol> border = 21-46 = -25px from item left.
+ Node (10px wide): left = -25 - 5 = -30px.
+ */
+ left: -30px;
+ width: 10px;
+ height: 10px;
+ border: 2px solid var(--bg);
+ }
+
+ @screen md {
+ .timeline-node {
+ width: 12px;
+ height: 12px;
+ top: 14px;
+ /* Desktop: spine center = 50% of item. Node center on spine: left = 50% - 6px */
+ left: calc(50% - 6px);
+ }
+ }
+
+ /* ---- Card wrapper ---- */
+ .timeline-card {
+ @apply flex border rounded-lg overflow-hidden bg-surface;
+ /* Mobile: column (thumb on top) */
+ flex-direction: column;
+ width: 100%;
+ transition: box-shadow 0.2s, transform 0.2s;
+ }
+
+ .timeline-card:hover {
+ transform: translateY(-2px);
+ }
+
+ @screen md {
+ /* Desktop: push card into left or right half via item padding */
+ .timeline-item--left {
+ padding-right: calc(50% + 24px);
+ }
+ .timeline-item--right {
+ padding-left: calc(50% + 24px);
+ }
+ /* Left card: thumb outer-left, body inner-right */
+ .timeline-item--left .timeline-card {
+ flex-direction: row;
+ }
+ /* Right card: body inner-left, thumb outer-right */
+ .timeline-item--right .timeline-card {
+ flex-direction: row-reverse;
+ }
+ }
+
+ /* ---- Thumbnail panel ---- */
+ .timeline-thumb {
+ @apply flex-shrink-0 overflow-hidden;
+ /* Mobile: full-width banner */
+ width: 100%;
+ height: 90px;
+ }
+
+ @screen md {
+ .timeline-thumb {
+ /* 3:2 landscape: wider than tall */
+ width: 40%;
+ height: auto;
+ aspect-ratio: 3 / 2;
+ }
+ }
+
+ .timeline-thumb img {
+ @apply w-full h-full object-cover;
+ transition: transform 0.2s;
+ }
+
+ .timeline-card:hover .timeline-thumb img {
+ transform: scale(1.03);
+ }
+
+ /* ---- Text panel ---- */
+ .timeline-body {
+ @apply flex flex-col gap-2 p-4 flex-1;
+ }
+
+ /* ---- Meta row (TYPE · date) ---- */
+ .timeline-meta {
+ @apply flex items-center gap-2 text-xs font-mono tracking-widest uppercase;
+ }
+
+ .timeline-meta-sep {
+ @apply text-border;
+ }
+
+ .timeline-date {
+ @apply text-text-dim normal-case tracking-normal;
+ }
+
+ /* ---- Title ---- */
+ .timeline-title {
+ @apply font-semibold text-base leading-snug;
+ }
+
+ .timeline-title a {
+ @apply hover:text-accent transition-colors;
+ }
+
+ /* ---- Excerpt ---- */
+ .timeline-excerpt {
+ @apply text-text-dim text-sm line-clamp-3 leading-relaxed;
+ }
+
+ /* ---- Pinned badge ---- */
+ .timeline-pinned {
+ @apply inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-semibold;
+ }
+
+ /* ---- Type-color variants (node, connector, card) ---- */
+
+ /* Tech (purple) */
+ .timeline-node--tech {
+ background-color: var(--type-tech);
+ box-shadow: 0 0 12px color-mix(in srgb, var(--type-tech) 40%, transparent);
+ }
+ .timeline-connector--tech {
+ background-color: var(--type-tech);
+ }
+ .timeline-card--tech {
+ border-color: color-mix(in srgb, var(--type-tech) 25%, transparent);
+ box-shadow: 0 0 18px color-mix(in srgb, var(--type-tech) 10%, transparent);
+ }
+
+ /* Life (amber) */
+ .timeline-node--life {
+ background-color: var(--type-life);
+ box-shadow: 0 0 12px color-mix(in srgb, var(--type-life) 40%, transparent);
+ }
+ .timeline-connector--life {
+ background-color: var(--type-life);
+ }
+ .timeline-card--life {
+ border-color: color-mix(in srgb, var(--type-life) 25%, transparent);
+ box-shadow: 0 0 18px color-mix(in srgb, var(--type-life) 10%, transparent);
+ }
+
+ /* Quote (green) */
+ .timeline-node--quote {
+ background-color: var(--type-quote);
+ box-shadow: 0 0 12px color-mix(in srgb, var(--type-quote) 40%, transparent);
+ }
+ .timeline-connector--quote {
+ background-color: var(--type-quote);
+ }
+ .timeline-card--quote {
+ border-color: color-mix(in srgb, var(--type-quote) 25%, transparent);
+ box-shadow: 0 0 18px color-mix(in srgb, var(--type-quote) 10%, transparent);
+ }
+
+ /* Link (cyan) */
+ .timeline-node--link {
+ background-color: var(--type-link);
+ box-shadow: 0 0 12px color-mix(in srgb, var(--type-link) 40%, transparent);
+ }
+ .timeline-connector--link {
+ background-color: var(--type-link);
+ }
+ .timeline-card--link {
+ border-color: color-mix(in srgb, var(--type-link) 25%, transparent);
+ box-shadow: 0 0 18px color-mix(in srgb, var(--type-link) 10%, transparent);
+ }
+
+ /* Photo (pink) */
+ .timeline-node--photo {
+ background-color: var(--type-photo);
+ box-shadow: 0 0 12px color-mix(in srgb, var(--type-photo) 40%, transparent);
+ }
+ .timeline-connector--photo {
+ background-color: var(--type-photo);
+ }
+ .timeline-card--photo {
+ border-color: color-mix(in srgb, var(--type-photo) 25%, transparent);
+ box-shadow: 0 0 18px color-mix(in srgb, var(--type-photo) 10%, transparent);
+ }
+
+ /* ---- Timeline lazy-reveal (scroll-triggered) ---- */
+ .js-lazy-timeline > .timeline-item {
+ opacity: 0;
+ transform: translateX(-18px);
+ transition: opacity 320ms ease-out, transform 320ms ease-out;
+ }
+
+ @screen md {
+ .js-lazy-timeline > .timeline-item--right {
+ transform: translateX(18px);
+ }
+ }
+
+ .js-lazy-timeline > .timeline-item.is-visible {
+ opacity: 1;
+ transform: translateX(0);
+ }
+
+ /* Header navigation styling */
+ .header {
+ @apply fixed top-0 left-0 right-0 z-40;
+ }
+
+ .header-nav {
+ @apply hidden md:flex items-center gap-6;
+ }
+
+ .nav-link {
+ @apply text-text hover:text-accent transition-colors;
+ }
+
+ .header-actions {
+ @apply flex items-center gap-4;
+ }
+
+ /* Mobile menu overlay */
+ .menu-overlay {
+ @apply fixed inset-0 bg-bg z-40 opacity-0 invisible transition-all duration-300;
+ }
+
+ .menu-overlay.active {
+ @apply opacity-100 visible;
+ }
+
+ .menu-nav {
+ @apply flex flex-col gap-4 p-6 text-lg font-semibold;
+ }
+
+ .menu-nav a {
+ @apply text-text hover:text-accent transition-colors;
+ }
+
+ /* Breadcrumb navigation */
+ .breadcrumb {
+ @apply flex items-center gap-2 text-sm text-text-dim;
+ }
+
+ .breadcrumb a {
+ @apply hover:text-accent transition-colors;
+ }
+
+ .breadcrumb-separator {
+ @apply opacity-50;
+ }
+
+ /* Article metadata styling (with icons) */
+ .article-meta {
+ @apply flex flex-wrap items-center gap-4 text-sm text-text-dim;
+ }
+
+ .article-meta-item {
+ @apply flex items-center gap-2;
+ }
+
+ .article-meta-item i {
+ @apply w-4 h-4 flex-shrink-0;
+ color: var(--accent2);
+ }
+
+ /* Hero typography with fluid sizing */
+ .hero-title {
+ font-size: clamp(2rem, 5vw + 1rem, 4.5rem);
+ }
+
+ .section-title {
+ font-size: clamp(1.5rem, 3vw + 0.5rem, 2.5rem);
+ }
+
+ /* ---- Article prev/next navigation ---- */
+ .article-nav {
+ @apply border-t border-border pt-6;
+ }
+
+ .article-nav-prompt {
+ @apply font-mono text-sm mb-2;
+ color: var(--accent);
+ }
+
+ .article-nav-links {
+ @apply flex flex-col md:flex-row md:justify-between md:items-center gap-4 md:gap-0 font-mono text-sm;
+ }
+
+ .article-nav-link {
+ @apply hover:text-accent transition-colors text-text;
+ }
+
+ .article-nav-placeholder {
+ @apply text-text-dim opacity-40;
+ }
+
+ /* ---- Footer badge variants ---- */
+ .badge-footer-accent {
+ @apply inline-flex items-center px-2.5 py-1 rounded text-xs font-mono font-semibold whitespace-nowrap;
+ border: 1px solid rgba(168, 85, 247, 0.35);
+ background: rgba(168, 85, 247, 0.1);
+ color: var(--accent);
+ }
+
+ .badge-footer-accent2 {
+ @apply inline-flex items-center px-2.5 py-1 rounded text-xs font-mono font-semibold whitespace-nowrap;
+ border: 1px solid rgba(0, 255, 136, 0.35);
+ background: rgba(0, 255, 136, 0.1);
+ color: var(--accent2);
+ }
+
+ /* Back to top button */
+ .back-to-top {
+ @apply fixed bottom-6 right-6 z-40 w-11 h-11 rounded-full flex items-center justify-center;
+ background: var(--accent);
+ box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.4);
+ transition: background 200ms ease, box-shadow 200ms ease;
+ color: #fff;
+ }
+ .back-to-top:hover {
+ background: var(--accent);
+ filter: brightness(0.85);
+ box-shadow: 0 0 20px var(--accent);
+ }
+ .back-to-top:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+/* Prose overrides for light theme */
+html.theme-light .prose,
+html.theme-light .prose-invert {
color: var(--text);
- font-family: var(--font-body);
- font-size: var(--fs-body);
- line-height: 1.95;
- transition: background-color 0.2s, color 0.2s;
- position: relative;
}
-/* Dot grid background (shared across all pages) */
-body::before {
- content: '';
+html.theme-light .prose a,
+html.theme-light .prose-invert a {
+ color: var(--accent);
+}
+
+html.theme-light .prose strong,
+html.theme-light .prose-invert strong {
+ color: var(--text);
+}
+
+html.theme-light .prose code,
+html.theme-light .prose-invert code {
+ color: var(--accent2);
+}
+
+html.theme-light .prose pre,
+html.theme-light .prose-invert pre {
+ background-color: var(--surface);
+ color: var(--text);
+}
+
+html.theme-light .prose h1,
+html.theme-light .prose h2,
+html.theme-light .prose h3,
+html.theme-light .prose h4,
+html.theme-light .prose h5,
+html.theme-light .prose h6,
+html.theme-light .prose-invert h1,
+html.theme-light .prose-invert h2,
+html.theme-light .prose-invert h3,
+html.theme-light .prose-invert h4,
+html.theme-light .prose-invert h5,
+html.theme-light .prose-invert h6 {
+ color: var(--text);
+}
+
+html.theme-light .prose blockquote,
+html.theme-light .prose-invert blockquote {
+ color: var(--text);
+ border-left-color: var(--accent);
+}
+
+/* Responsive container utilities - mobile-first */
+.container {
+ @apply max-w-full px-4;
+}
+
+@media (min-width: 768px) {
+ .container {
+ @apply max-w-4xl px-6;
+ }
+}
+
+@media (min-width: 1060px) {
+ .container {
+ @apply max-w-5xl px-8;
+ }
+}
+
+/* Alpine.js x-cloak - hide content until Alpine initializes */
+[x-cloak] {
+ display: none !important;
+}
+
+/* Respect user's motion preferences */
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Matrix rain canvas background */
+#matrix-rain {
position: fixed;
inset: 0;
- z-index: var(--z-matrix);
+ width: 100%;
+ height: 100%;
pointer-events: none;
- background-image: radial-gradient(circle, rgba(168, 85, 247, 0.07) 1px, transparent 1px);
- background-size: 30px 30px;
+ z-index: 1;
}
-html.theme-light body::before {
- background-image: radial-gradient(circle, rgba(124, 58, 237, 0.05) 1px, transparent 1px);
+/* Dark theme: 13% opacity (inner pages) */
+html.theme-dark #matrix-rain {
+ opacity: 0.13;
}
-/* Typography */
-h1 {
- font-family: var(--font-head);
- font-size: var(--fs-h2);
- font-weight: 800;
- line-height: 1.1;
- margin-bottom: 0.5rem;
+/* Light theme: 18% opacity (inner pages) */
+html.theme-light #matrix-rain {
+ opacity: 0.18;
}
-h2 {
- font-family: var(--font-head);
- font-size: var(--fs-h2);
- font-weight: 800;
- line-height: 1.1;
- margin: 2rem 0 1rem;
+/* Homepage: more prominent background */
+html.theme-dark body[data-page-kind="home"] #matrix-rain {
+ opacity: 0.28;
}
-h3 {
- font-family: var(--font-head);
- font-size: var(--fs-h3);
- font-weight: 800;
- line-height: 1.2;
- margin: 1.5rem 0 0.75rem;
+html.theme-light body[data-page-kind="home"] #matrix-rain {
+ opacity: 0.35;
}
-h4, h5, h6 {
- font-family: var(--font-head);
- font-weight: 800;
- margin: 1rem 0 0.5rem;
+/* Reduced motion: hide canvas entirely */
+@media (prefers-reduced-motion: reduce) {
+ #matrix-rain {
+ display: none;
+ }
}
-p {
- margin-bottom: 1rem;
+/* Content grid background — blocks rain under text, visible in gutters (single pages only) */
+.content-grid {
+ position: relative;
+ z-index: 10;
+ background-color: var(--bg);
+ padding: 2px;
+ border: 1px solid var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
}
-a {
- color: var(--accent);
- text-decoration: none;
- transition: var(--transition);
+@media (min-width: 768px) {
+ .content-grid {
+ padding: 2rem;
+ }
}
-a:hover {
- color: var(--accent2);
+/* Article list items — soft glow effect */
+article.border.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
}
-/* Utilities */
-.container {
- max-width: var(--container-max);
- margin: 0 auto;
- padding: 0 1.5rem;
+/* ============================================
+ FORM COMPONENTS (Week 4)
+ ============================================ */
+
+/* Form input base styles */
+.form-input,
+.form-textarea,
+.form-select {
+ @apply w-full px-4 py-2 rounded border border-border bg-bg2 text-text font-body transition-all duration-200;
}
-.container-narrow {
- max-width: var(--container-narrow);
- margin: 0 auto;
- padding: 0 1.5rem;
+/* Input placeholder styling */
+.form-input::placeholder,
+.form-textarea::placeholder {
+ color: var(--text-dim);
+ opacity: 0.7;
}
-/* Section layout helpers */
-.section-header { margin-bottom: 3rem; }
+/* Input focus state */
+.form-input:focus,
+.form-textarea:focus,
+.form-select:focus {
+ @apply outline-none border-accent ring-2 ring-accent ring-offset-2;
+ ring-offset-color: var(--bg);
+ border-color: var(--accent);
+}
-.section-eyebrow {
- font-family: var(--font-mono);
- font-size: 0.75rem;
- letter-spacing: 0.16em;
- text-transform: uppercase;
- color: var(--accent);
- margin-bottom: 0.5rem;
+/* Input invalid/error state */
+.form-input:invalid,
+.form-textarea:invalid,
+.form-select:invalid,
+.form-input.error,
+.form-textarea.error,
+.form-select.error {
+ @apply border-red-500 ring-red-500;
}
-.section-title {
- font-family: var(--font-head);
- font-size: clamp(1.5rem, 4vw, 2.5rem);
- font-weight: 800;
- color: var(--text);
- line-height: 1.1;
+.form-input:invalid:focus,
+.form-textarea:invalid:focus,
+.form-select:invalid:focus {
+ @apply ring-red-500 border-red-500;
+ ring-offset-color: var(--bg);
}
-/* Scroll-reveal base states (used by scroll-reveal.js) */
-.reveal {
- opacity: 0;
- transform: translateY(28px);
- transition: opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
- transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
+/* Input disabled state */
+.form-input:disabled,
+.form-textarea:disabled,
+.form-select:disabled {
+ @apply opacity-50 cursor-not-allowed bg-muted;
}
-.reveal.revealed {
- opacity: 1;
- transform: translateY(0);
+/* Textarea specific */
+.form-textarea {
+ @apply min-h-32;
+ resize: vertical;
}
-@media (prefers-reduced-motion: reduce) {
- .reveal { opacity: 1; transform: none; transition: none; }
+.form-textarea.auto-expand {
+ resize: none;
+ overflow-y: hidden;
+}
+
+/* Select dropdown */
+.form-select {
+ cursor: pointer;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a855f7' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ padding-right: 2.5rem;
+}
+
+html.theme-light .form-select {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239333ea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+}
+
+/* Checkbox and radio button base */
+.form-checkbox,
+.form-radio {
+ @apply w-5 h-5 cursor-pointer accent-accent transition-all duration-200;
+ appearance: none;
+ border: 2px solid var(--border);
+ border-radius: 0.375rem;
+ flex-shrink: 0;
+}
+
+.form-radio {
+ border-radius: 50%;
+}
+
+/* Checkbox/radio focus state */
+.form-checkbox:focus-visible,
+.form-radio:focus-visible {
+ @apply outline-none ring-2 ring-accent ring-offset-2;
+ ring-offset-color: var(--bg);
+}
+
+/* Checkbox/radio checked state */
+.form-checkbox:checked,
+.form-radio:checked {
+ background-color: var(--accent);
+ border-color: var(--accent);
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100%;
+}
+
+.form-radio:checked {
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3.5'/%3E%3C/svg%3E");
}
-main {
- min-height: calc(100vh - 200px);
+/* Checkbox/radio disabled state */
+.form-checkbox:disabled,
+.form-radio:disabled {
+ @apply opacity-50 cursor-not-allowed;
+ border-color: var(--muted);
}
-/* Lists */
-ul, ol {
- margin-left: 1.5rem;
- margin-bottom: 1rem;
+/* Form group layout */
+.form-group {
+ @apply space-y-2;
}
-li {
- margin-bottom: 0.5rem;
+.form-group label {
+ @apply block text-sm font-semibold text-text;
}
-code {
- font-family: var(--font-mono);
- background: var(--surface);
- padding: 0.25rem 0.5rem;
- border-radius: 4px;
- font-size: 0.9em;
+.form-group.required label::after {
+ content: ' *';
+ color: #ef4444;
}
-pre {
- margin-bottom: 1rem;
- overflow-x: auto;
+.form-group-input {
+ @apply relative;
}
-pre code {
- padding: 0;
- background: none;
- border-radius: 0;
+.form-group-help {
+ @apply text-xs text-text-dim;
}
-/* Focus styles for accessibility */
-:focus {
+.form-group.error .form-group-help {
+ @apply text-red-500;
+}
+
+.form-error {
+ @apply text-sm text-red-500 mt-1;
+}
+
+/* Form layout utilities */
+.form-row {
+ @apply flex flex-col md:flex-row gap-4;
+}
+
+.form-row > .form-group {
+ @apply flex-1;
+}
+
+.form-inline {
+ @apply flex flex-col sm:flex-row items-end gap-4;
+}
+
+.form-inline .form-group {
+ @apply flex-1;
+}
+
+.form-inline .btn {
+ @apply h-10;
+}
+
+/* Character count indicator */
+.form-char-count {
+ @apply text-xs text-text-dim text-right;
+}
+
+.form-char-count.warning {
+ @apply text-amber-500;
+}
+
+.form-char-count.error {
+ @apply text-red-500;
+}
+
+/* ============================================
+ FOCUS MANAGEMENT (Week 5)
+ ============================================ */
+
+/* Enhanced :focus-visible with accent color styling */
+:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
-button:focus,
-a:focus {
- outline: 2px dashed var(--accent);
- outline-offset: 4px;
+/* Respect motion preferences for focus indicator */
+@media (prefers-reduced-motion: reduce) {
+ :focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+/* Button and link hover/focus transitions */
+button,
+a.btn,
+.btn {
+ transition: all 150ms ease-out;
+}
+
+button:hover,
+a.btn:hover,
+.btn:hover {
+ opacity: 0.8;
+ transform: translateY(-1px);
+}
+
+button:active,
+a.btn:active,
+.btn:active {
+ transform: translateY(0);
+}
+
+/* Form input focus transitions with glow effect */
+input,
+textarea,
+select {
+ transition: all 200ms ease-out;
+}
+
+input:focus,
+textarea:focus,
+select:focus,
+input:focus-visible,
+textarea:focus-visible,
+select:focus-visible {
+ box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.1);
+}
+
+/* ============================================
+ MODAL COMPONENTS (Week 4)
+ ============================================ */
+
+/* Modal backdrop */
+.modal-backdrop {
+ @apply fixed inset-0 bg-black/50 opacity-0 invisible transition-all duration-300 z-40;
+ backdrop-filter: blur(2px);
+}
+
+.modal-backdrop.active {
+ @apply opacity-100 visible;
+}
+
+/* Modal container */
+.modal {
+ @apply fixed inset-0 flex items-center justify-center opacity-0 invisible transition-all duration-300 z-50 p-4;
+ pointer-events: none;
+}
+
+.modal.active {
+ @apply opacity-100 visible;
+ pointer-events: auto;
+}
+
+.modal.active .modal-backdrop {
+ @apply opacity-100 visible;
+ pointer-events: auto;
+}
+
+/* Modal content box */
+.modal-content {
+ @apply bg-bg2 border border-border rounded-lg shadow-2xl max-w-lg w-full max-h-[90vh] flex flex-col;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ animation: modalSlideUp 0.3s ease-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes modalSlideUp {
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Modal header */
+.modal-header {
+ @apply flex items-start justify-between gap-4 p-6 border-b border-border;
+}
+
+.modal-title {
+ @apply text-xl font-bold text-text;
+}
+
+.modal-close {
+ @apply flex items-center justify-center w-8 h-8 rounded hover:bg-surface cursor-pointer transition-colors;
+}
+
+.modal-close::before,
+.modal-close::after {
+ content: '';
+ @apply absolute w-5 h-0.5 bg-text-dim;
+}
+
+.modal-close::before {
+ transform: rotate(45deg);
+}
+
+.modal-close::after {
+ transform: rotate(-45deg);
+}
+
+.modal-close:hover::before,
+.modal-close:hover::after {
+ @apply bg-accent;
+}
+
+/* Modal body */
+.modal-body {
+ @apply flex-1 overflow-y-auto p-6 space-y-4;
+}
+
+/* Modal footer */
+.modal-footer {
+ @apply flex items-center justify-end gap-3 p-6 border-t border-border;
+}
+
+/* Modal sizes */
+.modal-content.modal-sm {
+ @apply max-w-sm;
+}
+
+.modal-content.modal-md {
+ @apply max-w-md;
+}
+
+.modal-content.modal-lg {
+ @apply max-w-2xl;
+}
+
+/* Modal variants */
+.modal-content.modal-alert {
+ @apply max-w-sm;
+}
+
+.modal-content.modal-confirm {
+ @apply max-w-sm;
}
-/* Reduced motion support */
+/* Modal button styling */
+.modal-footer .btn {
+ @apply min-w-[100px];
+}
+
+/* ============================================
+ INTERACTIVE PATTERNS (Week 4)
+ ============================================ */
+
+/* Loading spinner */
+.spinner {
+ @apply inline-block;
+ width: 1rem;
+ height: 1rem;
+ border: 2px solid var(--border);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.spinner-sm {
+ width: 0.75rem;
+ height: 0.75rem;
+ border-width: 1.5px;
+}
+
+.spinner-lg {
+ width: 1.5rem;
+ height: 1.5rem;
+ border-width: 3px;
+}
+
+/* Button with spinner */
+.btn:disabled .spinner {
+ @apply inline-block mr-2;
+}
+
+/* Toast notification container */
+.toast-container {
+ @apply fixed bottom-0 right-0 p-4 space-y-3 max-w-sm z-50;
+}
+
+@media (max-width: 640px) {
+ .toast-container {
+ @apply left-0 right-0 px-4;
+ }
+}
+
+/* Toast base */
+.toast {
+ @apply flex items-start gap-3 p-4 rounded-lg border border-border shadow-lg;
+ animation: slideInUp 0.3s ease-out;
+ background-color: var(--bg2);
+ color: var(--text);
+}
+
+@keyframes slideInUp {
+ from {
+ transform: translateY(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Animation Utility Classes */
+
+.animate-fade-in {
+ animation: fadeIn 300ms ease-out;
+}
+
+.animate-slide-up {
+ animation: slideUp 300ms ease-out;
+}
+
+.animate-spin-loader {
+ animation: spin 600ms linear infinite;
+}
+
+/* Toast variants */
+.toast-success {
+ border-color: #10b981;
+ background-color: rgba(16, 185, 129, 0.1);
+}
+
+.toast-success::before {
+ content: '✓';
+ color: #10b981;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+
+.toast-error {
+ border-color: #ef4444;
+ background-color: rgba(239, 68, 68, 0.1);
+}
+
+.toast-error::before {
+ content: '✕';
+ color: #ef4444;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+
+.toast-info {
+ border-color: #3b82f6;
+ background-color: rgba(59, 130, 246, 0.1);
+}
+
+.toast-info::before {
+ content: 'ℹ';
+ color: #3b82f6;
+ flex-shrink: 0;
+}
+
+.toast-warning {
+ border-color: #f59e0b;
+ background-color: rgba(245, 158, 11, 0.1);
+}
+
+.toast-warning::before {
+ content: '⚠';
+ color: #f59e0b;
+ flex-shrink: 0;
+}
+
+.toast-close {
+ @apply ml-auto flex-shrink-0 w-6 h-6 flex items-center justify-center cursor-pointer hover:bg-surface rounded transition-colors;
+}
+
+/* Tooltip */
+.tooltip {
+ @apply relative inline-block;
+}
+
+.tooltip-text {
+ @apply absolute bg-bg2 text-text-dim text-xs rounded px-2 py-1 whitespace-nowrap pointer-events-none opacity-0 invisible transition-all duration-200;
+ z-index: 50;
+ bottom: 125%;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.tooltip:hover .tooltip-text {
+ @apply opacity-100 visible;
+}
+
+.tooltip-text::after {
+ content: '';
+ @apply absolute w-2 h-2 bg-bg2;
+ bottom: -4px;
+ left: 50%;
+ transform: translateX(-50%) rotate(45deg);
+}
+
+/* Tooltip directions */
+.tooltip-bottom .tooltip-text {
+ @apply top-[125%] bottom-auto;
+}
+
+.tooltip-bottom .tooltip-text::after {
+ @apply top-[-4px] bottom-auto;
+ transform: translateX(-50%) rotate(225deg);
+}
+
+.tooltip-left .tooltip-text {
+ @apply left-auto right-[125%];
+ transform: none;
+}
+
+.tooltip-left .tooltip-text::after {
+ @apply left-auto right-[-4px];
+ transform: rotate(135deg);
+}
+
+.tooltip-right .tooltip-text {
+ @apply left-[125%];
+ transform: none;
+}
+
+.tooltip-right .tooltip-text::after {
+ @apply left-[-4px] right-auto;
+ transform: rotate(315deg);
+}
+
+/* Motion Safety - Respect prefers-reduced-motion */
+
@media (prefers-reduced-motion: reduce) {
- * {
- animation-duration: 0.01ms !important;
- animation-iteration-count: 1 !important;
- transition-duration: 0.01ms !important;
- scroll-behavior: auto !important;
+ /* Remove all animations */
+ *,
+ *::before,
+ *::after {
+ animation: none !important;
+ transition: none !important;
+ }
+
+ /* Ensure focus-visible is still visible */
+ :focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
}
}
diff --git a/assets/css/main.min.css b/assets/css/main.min.css
new file mode 100644
index 0000000..9fbcaa0
--- /dev/null
+++ b/assets/css/main.min.css
@@ -0,0 +1,4495 @@
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+ --tw-contain-size: ;
+ --tw-contain-layout: ;
+ --tw-contain-paint: ;
+ --tw-contain-style: ;
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+ --tw-contain-size: ;
+ --tw-contain-layout: ;
+ --tw-contain-paint: ;
+ --tw-contain-style: ;
+}
+
+/*
+! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com
+*/
+
+/*
+1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
+2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
+*/
+
+*,
+::before,
+::after {
+ box-sizing: border-box;
+ /* 1 */
+ border-width: 0;
+ /* 2 */
+ border-style: solid;
+ /* 2 */
+ border-color: #e5e7eb;
+ /* 2 */
+}
+
+::before,
+::after {
+ --tw-content: '';
+}
+
+/*
+1. Use a consistent sensible line-height in all browsers.
+2. Prevent adjustments of font size after orientation changes in iOS.
+3. Use a more readable tab size.
+4. Use the user's configured `sans` font-family by default.
+5. Use the user's configured `sans` font-feature-settings by default.
+6. Use the user's configured `sans` font-variation-settings by default.
+7. Disable tap highlights on iOS
+*/
+
+html,
+:host {
+ line-height: 1.5;
+ /* 1 */
+ -webkit-text-size-adjust: 100%;
+ /* 2 */
+ -moz-tab-size: 4;
+ /* 3 */
+ -o-tab-size: 4;
+ tab-size: 4;
+ /* 3 */
+ font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ /* 4 */
+ font-feature-settings: normal;
+ /* 5 */
+ font-variation-settings: normal;
+ /* 6 */
+ -webkit-tap-highlight-color: transparent;
+ /* 7 */
+}
+
+/*
+1. Remove the margin in all browsers.
+2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
+*/
+
+body {
+ margin: 0;
+ /* 1 */
+ line-height: inherit;
+ /* 2 */
+}
+
+/*
+1. Add the correct height in Firefox.
+2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
+3. Ensure horizontal rules are visible by default.
+*/
+
+hr {
+ height: 0;
+ /* 1 */
+ color: inherit;
+ /* 2 */
+ border-top-width: 1px;
+ /* 3 */
+}
+
+/*
+Add the correct text decoration in Chrome, Edge, and Safari.
+*/
+
+abbr:where([title]) {
+ -webkit-text-decoration: underline dotted;
+ text-decoration: underline dotted;
+}
+
+/*
+Remove the default font size and weight for headings.
+*/
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-size: inherit;
+ font-weight: inherit;
+}
+
+/*
+Reset links to optimize for opt-in styling instead of opt-out.
+*/
+
+a {
+ color: inherit;
+ text-decoration: inherit;
+}
+
+/*
+Add the correct font weight in Edge and Safari.
+*/
+
+b,
+strong {
+ font-weight: bolder;
+}
+
+/*
+1. Use the user's configured `mono` font-family by default.
+2. Use the user's configured `mono` font-feature-settings by default.
+3. Use the user's configured `mono` font-variation-settings by default.
+4. Correct the odd `em` font sizing in all browsers.
+*/
+
+code,
+kbd,
+samp,
+pre {
+ font-family: JetBrains Mono, monospace;
+ /* 1 */
+ font-feature-settings: normal;
+ /* 2 */
+ font-variation-settings: normal;
+ /* 3 */
+ font-size: 1em;
+ /* 4 */
+}
+
+/*
+Add the correct font size in all browsers.
+*/
+
+small {
+ font-size: 80%;
+}
+
+/*
+Prevent `sub` and `sup` elements from affecting the line height in all browsers.
+*/
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+sup {
+ top: -0.5em;
+}
+
+/*
+1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
+2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
+3. Remove gaps between table borders by default.
+*/
+
+table {
+ text-indent: 0;
+ /* 1 */
+ border-color: inherit;
+ /* 2 */
+ border-collapse: collapse;
+ /* 3 */
+}
+
+/*
+1. Change the font styles in all browsers.
+2. Remove the margin in Firefox and Safari.
+3. Remove default padding in all browsers.
+*/
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ font-family: inherit;
+ /* 1 */
+ font-feature-settings: inherit;
+ /* 1 */
+ font-variation-settings: inherit;
+ /* 1 */
+ font-size: 100%;
+ /* 1 */
+ font-weight: inherit;
+ /* 1 */
+ line-height: inherit;
+ /* 1 */
+ letter-spacing: inherit;
+ /* 1 */
+ color: inherit;
+ /* 1 */
+ margin: 0;
+ /* 2 */
+ padding: 0;
+ /* 3 */
+}
+
+/*
+Remove the inheritance of text transform in Edge and Firefox.
+*/
+
+button,
+select {
+ text-transform: none;
+}
+
+/*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Remove default button styles.
+*/
+
+button,
+input:where([type='button']),
+input:where([type='reset']),
+input:where([type='submit']) {
+ -webkit-appearance: button;
+ /* 1 */
+ background-color: transparent;
+ /* 2 */
+ background-image: none;
+ /* 2 */
+}
+
+/*
+Use the modern Firefox focus style for all focusable elements.
+*/
+
+:-moz-focusring {
+ outline: auto;
+}
+
+/*
+Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
+*/
+
+:-moz-ui-invalid {
+ box-shadow: none;
+}
+
+/*
+Add the correct vertical alignment in Chrome and Firefox.
+*/
+
+progress {
+ vertical-align: baseline;
+}
+
+/*
+Correct the cursor style of increment and decrement buttons in Safari.
+*/
+
+::-webkit-inner-spin-button,
+::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/*
+1. Correct the odd appearance in Chrome and Safari.
+2. Correct the outline style in Safari.
+*/
+
+[type='search'] {
+ -webkit-appearance: textfield;
+ /* 1 */
+ outline-offset: -2px;
+ /* 2 */
+}
+
+/*
+Remove the inner padding in Chrome and Safari on macOS.
+*/
+
+::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/*
+1. Correct the inability to style clickable types in iOS and Safari.
+2. Change font properties to `inherit` in Safari.
+*/
+
+::-webkit-file-upload-button {
+ -webkit-appearance: button;
+ /* 1 */
+ font: inherit;
+ /* 2 */
+}
+
+/*
+Add the correct display in Chrome and Safari.
+*/
+
+summary {
+ display: list-item;
+}
+
+/*
+Removes the default spacing and border for appropriate elements.
+*/
+
+blockquote,
+dl,
+dd,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+hr,
+figure,
+p,
+pre {
+ margin: 0;
+}
+
+fieldset {
+ margin: 0;
+ padding: 0;
+}
+
+legend {
+ padding: 0;
+}
+
+ol,
+ul,
+menu {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+/*
+Reset default styling for dialogs.
+*/
+
+dialog {
+ padding: 0;
+}
+
+/*
+Prevent resizing textareas horizontally by default.
+*/
+
+textarea {
+ resize: vertical;
+}
+
+/*
+1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
+2. Set the default placeholder color to the user's configured gray 400 color.
+*/
+
+input::-moz-placeholder, textarea::-moz-placeholder {
+ opacity: 1;
+ /* 1 */
+ color: #9ca3af;
+ /* 2 */
+}
+
+input::placeholder,
+textarea::placeholder {
+ opacity: 1;
+ /* 1 */
+ color: #9ca3af;
+ /* 2 */
+}
+
+/*
+Set the default cursor for buttons.
+*/
+
+button,
+[role="button"] {
+ cursor: pointer;
+}
+
+/*
+Make sure disabled buttons don't get the pointer cursor.
+*/
+
+:disabled {
+ cursor: default;
+}
+
+/*
+1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
+2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
+ This can trigger a poorly considered lint error in some tools but is included by design.
+*/
+
+img,
+svg,
+video,
+canvas,
+audio,
+iframe,
+embed,
+object {
+ display: block;
+ /* 1 */
+ vertical-align: middle;
+ /* 2 */
+}
+
+/*
+Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
+*/
+
+img,
+video {
+ max-width: 100%;
+ height: auto;
+}
+
+/* Make elements with the HTML hidden attribute stay hidden by default */
+
+[hidden]:where(:not([hidden="until-found"])) {
+ display: none;
+}
+
+html {
+ overflow-x: hidden;
+}
+
+body {
+ overflow-x: hidden;
+ background-color: var(--bg);
+ font-family: IBM Plex Sans, sans-serif;
+ color: var(--text);
+}
+
+articlebody.border.border-border\/30.rounded-lg.overflow-hidden.group {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ font-weight: 700;
+ font-family: 'Oxanium', monospace;
+}
+
+h1 {
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+}
+
+@media (min-width: 768px) {
+ h1 {
+ font-size: 2.25rem;
+ line-height: 2.5rem;
+ }
+}
+
+h2 {
+ font-size: 1.5rem;
+ line-height: 2rem;
+}
+
+@media (min-width: 768px) {
+ h2 {
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+ }
+}
+
+h3 {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+}
+
+@media (min-width: 768px) {
+ h3 {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ }
+}
+
+.heading-prefix {
+ font-family: 'JetBrains Mono', monospace;
+ font-weight: 400;
+ font-size: 0.8em;
+ color: var(--accent);
+ opacity: 0.7;
+ margin-right: 0.35em;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+a {
+ color: var(--accent);
+ transition-property: opacity;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+a:hover {
+ opacity: 0.8;
+}
+
+code {
+ border-radius: 0.25rem;
+ border-width: 1px;
+ border-color: var(--border);
+ background-color: var(--surface);
+ padding-left: 0.375rem;
+ padding-right: 0.375rem;
+ padding-top: 0.125rem;
+ padding-bottom: 0.125rem;
+ font-family: JetBrains Mono, monospace;
+ color: var(--accent2);
+}
+
+articlecode.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+pre {
+ background-color: rgba(var(--surface-rgb), 0.8);
+ overflow-x: auto;
+ border-radius: 0.25rem;
+ border-width: 1px;
+ border-color: var(--border);
+ padding: 1rem;
+}
+
+articlepre.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+pre code {
+ border-width: 0px;
+ background-color: transparent;
+ padding: 0px;
+ color: var(--text);
+}
+
+*:focus-visible {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+ --tw-ring-color: var(--accent);
+ --tw-ring-offset-width: 2px;
+ ring-offset-color: var(--bg);
+}
+
+button,
+ input,
+ textarea,
+ select {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+.container {
+ width: 100%;
+}
+
+@media (min-width: 640px) {
+ .container {
+ max-width: 640px;
+ }
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 768px;
+ }
+}
+
+@media (min-width: 1060px) {
+ .container {
+ max-width: 1060px;
+ }
+}
+
+@media (min-width: 1280px) {
+ .container {
+ max-width: 1280px;
+ }
+}
+
+@media (min-width: 1536px) {
+ .container {
+ max-width: 1536px;
+ }
+}
+
+.prose {
+ color: var(--tw-prose-body);
+ max-width: 65ch;
+}
+
+.prose :where(p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 1.25em;
+ margin-bottom: 1.25em;
+}
+
+.prose :where([class~="lead"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-lead);
+ font-size: 1.25em;
+ line-height: 1.6;
+ margin-top: 1.2em;
+ margin-bottom: 1.2em;
+}
+
+.prose :where(a):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-links);
+ text-decoration: underline;
+ font-weight: 500;
+}
+
+.prose :where(strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-bold);
+ font-weight: 600;
+}
+
+.prose :where(a strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(blockquote strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(thead th strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: decimal;
+ margin-top: 1.25em;
+ margin-bottom: 1.25em;
+ padding-inline-start: 1.625em;
+}
+
+.prose :where(ol[type="A"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: upper-alpha;
+}
+
+.prose :where(ol[type="a"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: lower-alpha;
+}
+
+.prose :where(ol[type="A" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: upper-alpha;
+}
+
+.prose :where(ol[type="a" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: lower-alpha;
+}
+
+.prose :where(ol[type="I"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: upper-roman;
+}
+
+.prose :where(ol[type="i"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: lower-roman;
+}
+
+.prose :where(ol[type="I" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: upper-roman;
+}
+
+.prose :where(ol[type="i" s]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: lower-roman;
+}
+
+.prose :where(ol[type="1"]):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: decimal;
+}
+
+.prose :where(ul):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ list-style-type: disc;
+ margin-top: 1.25em;
+ margin-bottom: 1.25em;
+ padding-inline-start: 1.625em;
+}
+
+.prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker {
+ font-weight: 400;
+ color: var(--tw-prose-counters);
+}
+
+.prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *))::marker {
+ color: var(--tw-prose-bullets);
+}
+
+.prose :where(dt):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-headings);
+ font-weight: 600;
+ margin-top: 1.25em;
+}
+
+.prose :where(hr):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ border-color: var(--tw-prose-hr);
+ border-top-width: 1px;
+ margin-top: 3em;
+ margin-bottom: 3em;
+}
+
+.prose :where(blockquote):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ font-weight: 500;
+ font-style: italic;
+ color: var(--tw-prose-quotes);
+ border-inline-start-width: 0.25rem;
+ border-inline-start-color: var(--tw-prose-quote-borders);
+ quotes: "\201C""\201D""\2018""\2019";
+ margin-top: 1.6em;
+ margin-bottom: 1.6em;
+ padding-inline-start: 1em;
+}
+
+.prose :where(blockquote p:first-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::before {
+ content: open-quote;
+}
+
+.prose :where(blockquote p:last-of-type):not(:where([class~="not-prose"],[class~="not-prose"] *))::after {
+ content: close-quote;
+}
+
+.prose :where(h1):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-headings);
+ font-weight: 800;
+ font-size: 2.25em;
+ margin-top: 0;
+ margin-bottom: 0.8888889em;
+ line-height: 1.1111111;
+}
+
+.prose :where(h1 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ font-weight: 900;
+ color: inherit;
+}
+
+.prose :where(h2):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-headings);
+ font-weight: 700;
+ font-size: 1.5em;
+ margin-top: 2em;
+ margin-bottom: 1em;
+ line-height: 1.3333333;
+}
+
+.prose :where(h2 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ font-weight: 800;
+ color: inherit;
+}
+
+.prose :where(h3):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-headings);
+ font-weight: 600;
+ font-size: 1.25em;
+ margin-top: 1.6em;
+ margin-bottom: 0.6em;
+ line-height: 1.6;
+}
+
+.prose :where(h3 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ font-weight: 700;
+ color: inherit;
+}
+
+.prose :where(h4):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-headings);
+ font-weight: 600;
+ margin-top: 1.5em;
+ margin-bottom: 0.5em;
+ line-height: 1.5;
+}
+
+.prose :where(h4 strong):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ font-weight: 700;
+ color: inherit;
+}
+
+.prose :where(img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 2em;
+ margin-bottom: 2em;
+}
+
+.prose :where(picture):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ display: block;
+ margin-top: 2em;
+ margin-bottom: 2em;
+}
+
+.prose :where(video):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 2em;
+ margin-bottom: 2em;
+}
+
+.prose :where(kbd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ font-weight: 500;
+ font-family: inherit;
+ color: var(--tw-prose-kbd);
+ box-shadow: 0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);
+ font-size: 0.875em;
+ border-radius: 0.3125rem;
+ padding-top: 0.1875em;
+ padding-inline-end: 0.375em;
+ padding-bottom: 0.1875em;
+ padding-inline-start: 0.375em;
+}
+
+.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-code);
+ font-weight: 600;
+ font-size: 0.875em;
+}
+
+.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before {
+ content: "`";
+}
+
+.prose :where(code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after {
+ content: "`";
+}
+
+.prose :where(a code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(h1 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(h2 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+ font-size: 0.875em;
+}
+
+.prose :where(h3 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+ font-size: 0.9em;
+}
+
+.prose :where(h4 code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(blockquote code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(thead th code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: inherit;
+}
+
+.prose :where(pre):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-pre-code);
+ background-color: var(--tw-prose-pre-bg);
+ overflow-x: auto;
+ font-weight: 400;
+ font-size: 0.875em;
+ line-height: 1.7142857;
+ margin-top: 1.7142857em;
+ margin-bottom: 1.7142857em;
+ border-radius: 0.375rem;
+ padding-top: 0.8571429em;
+ padding-inline-end: 1.1428571em;
+ padding-bottom: 0.8571429em;
+ padding-inline-start: 1.1428571em;
+}
+
+.prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ background-color: transparent;
+ border-width: 0;
+ border-radius: 0;
+ padding: 0;
+ font-weight: inherit;
+ color: inherit;
+ font-size: inherit;
+ font-family: inherit;
+ line-height: inherit;
+}
+
+.prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::before {
+ content: none;
+}
+
+.prose :where(pre code):not(:where([class~="not-prose"],[class~="not-prose"] *))::after {
+ content: none;
+}
+
+.prose :where(table):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ width: 100%;
+ table-layout: auto;
+ margin-top: 2em;
+ margin-bottom: 2em;
+ font-size: 0.875em;
+ line-height: 1.7142857;
+}
+
+.prose :where(thead):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ border-bottom-width: 1px;
+ border-bottom-color: var(--tw-prose-th-borders);
+}
+
+.prose :where(thead th):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-headings);
+ font-weight: 600;
+ vertical-align: bottom;
+ padding-inline-end: 0.5714286em;
+ padding-bottom: 0.5714286em;
+ padding-inline-start: 0.5714286em;
+}
+
+.prose :where(tbody tr):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ border-bottom-width: 1px;
+ border-bottom-color: var(--tw-prose-td-borders);
+}
+
+.prose :where(tbody tr:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ border-bottom-width: 0;
+}
+
+.prose :where(tbody td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ vertical-align: baseline;
+}
+
+.prose :where(tfoot):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ border-top-width: 1px;
+ border-top-color: var(--tw-prose-th-borders);
+}
+
+.prose :where(tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ vertical-align: top;
+}
+
+.prose :where(th, td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ text-align: start;
+}
+
+.prose :where(figure > *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.prose :where(figcaption):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ color: var(--tw-prose-captions);
+ font-size: 0.875em;
+ line-height: 1.4285714;
+ margin-top: 0.8571429em;
+}
+
+.prose {
+ --tw-prose-body: #374151;
+ --tw-prose-headings: #111827;
+ --tw-prose-lead: #4b5563;
+ --tw-prose-links: #111827;
+ --tw-prose-bold: #111827;
+ --tw-prose-counters: #6b7280;
+ --tw-prose-bullets: #d1d5db;
+ --tw-prose-hr: #e5e7eb;
+ --tw-prose-quotes: #111827;
+ --tw-prose-quote-borders: #e5e7eb;
+ --tw-prose-captions: #6b7280;
+ --tw-prose-kbd: #111827;
+ --tw-prose-kbd-shadows: rgb(17 24 39 / 10%);
+ --tw-prose-code: #111827;
+ --tw-prose-pre-code: #e5e7eb;
+ --tw-prose-pre-bg: #1f2937;
+ --tw-prose-th-borders: #d1d5db;
+ --tw-prose-td-borders: #e5e7eb;
+ --tw-prose-invert-body: #d1d5db;
+ --tw-prose-invert-headings: #fff;
+ --tw-prose-invert-lead: #9ca3af;
+ --tw-prose-invert-links: #fff;
+ --tw-prose-invert-bold: #fff;
+ --tw-prose-invert-counters: #9ca3af;
+ --tw-prose-invert-bullets: #4b5563;
+ --tw-prose-invert-hr: #374151;
+ --tw-prose-invert-quotes: #f3f4f6;
+ --tw-prose-invert-quote-borders: #374151;
+ --tw-prose-invert-captions: #9ca3af;
+ --tw-prose-invert-kbd: #fff;
+ --tw-prose-invert-kbd-shadows: rgb(255 255 255 / 10%);
+ --tw-prose-invert-code: #fff;
+ --tw-prose-invert-pre-code: #d1d5db;
+ --tw-prose-invert-pre-bg: rgb(0 0 0 / 50%);
+ --tw-prose-invert-th-borders: #4b5563;
+ --tw-prose-invert-td-borders: #374151;
+ font-size: 1rem;
+ line-height: 1.75;
+}
+
+.prose :where(picture > img):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.prose :where(li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0.5em;
+ margin-bottom: 0.5em;
+}
+
+.prose :where(ol > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ padding-inline-start: 0.375em;
+}
+
+.prose :where(ul > li):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ padding-inline-start: 0.375em;
+}
+
+.prose :where(.prose > ul > li p):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0.75em;
+ margin-bottom: 0.75em;
+}
+
+.prose :where(.prose > ul > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 1.25em;
+}
+
+.prose :where(.prose > ul > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-bottom: 1.25em;
+}
+
+.prose :where(.prose > ol > li > p:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 1.25em;
+}
+
+.prose :where(.prose > ol > li > p:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-bottom: 1.25em;
+}
+
+.prose :where(ul ul, ul ol, ol ul, ol ol):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0.75em;
+ margin-bottom: 0.75em;
+}
+
+.prose :where(dl):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 1.25em;
+ margin-bottom: 1.25em;
+}
+
+.prose :where(dd):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0.5em;
+ padding-inline-start: 1.625em;
+}
+
+.prose :where(hr + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0;
+}
+
+.prose :where(h2 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0;
+}
+
+.prose :where(h3 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0;
+}
+
+.prose :where(h4 + *):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0;
+}
+
+.prose :where(thead th:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ padding-inline-start: 0;
+}
+
+.prose :where(thead th:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ padding-inline-end: 0;
+}
+
+.prose :where(tbody td, tfoot td):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ padding-top: 0.5714286em;
+ padding-inline-end: 0.5714286em;
+ padding-bottom: 0.5714286em;
+ padding-inline-start: 0.5714286em;
+}
+
+.prose :where(tbody td:first-child, tfoot td:first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ padding-inline-start: 0;
+}
+
+.prose :where(tbody td:last-child, tfoot td:last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ padding-inline-end: 0;
+}
+
+.prose :where(figure):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 2em;
+ margin-bottom: 2em;
+}
+
+.prose :where(.prose > :first-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-top: 0;
+}
+
+.prose :where(.prose > :last-child):not(:where([class~="not-prose"],[class~="not-prose"] *)) {
+ margin-bottom: 0;
+}
+
+.prose-invert {
+ --tw-prose-body: var(--tw-prose-invert-body);
+ --tw-prose-headings: var(--tw-prose-invert-headings);
+ --tw-prose-lead: var(--tw-prose-invert-lead);
+ --tw-prose-links: var(--tw-prose-invert-links);
+ --tw-prose-bold: var(--tw-prose-invert-bold);
+ --tw-prose-counters: var(--tw-prose-invert-counters);
+ --tw-prose-bullets: var(--tw-prose-invert-bullets);
+ --tw-prose-hr: var(--tw-prose-invert-hr);
+ --tw-prose-quotes: var(--tw-prose-invert-quotes);
+ --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders);
+ --tw-prose-captions: var(--tw-prose-invert-captions);
+ --tw-prose-kbd: var(--tw-prose-invert-kbd);
+ --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows);
+ --tw-prose-code: var(--tw-prose-invert-code);
+ --tw-prose-pre-code: var(--tw-prose-invert-pre-code);
+ --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg);
+ --tw-prose-th-borders: var(--tw-prose-invert-th-borders);
+ --tw-prose-td-borders: var(--tw-prose-invert-td-borders);
+}
+
+.container {
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 56rem;
+}
+
+/* Background utilities */
+
+.bg-bg {
+ background-color: var(--bg);
+}
+
+.bg-surface {
+ background-color: var(--surface);
+}
+
+/* Border utilities */
+
+.border-border {
+ border-color: var(--border);
+}
+
+/* Text color utilities */
+
+.text-accent {
+ color: var(--accent);
+}
+
+.text-accent2 {
+ color: var(--accent2);
+}
+
+.text-text {
+ color: var(--text);
+}
+
+.text-text-dim {
+ color: var(--text-dim);
+}
+
+/* Additional semantic utilities */
+
+/* Glow effect utility */
+
+.glow-accent {
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+/* Frosted glass bar (header/footer) */
+
+.frosted-bar {
+ background-color: rgba(var(--bg2-rgb), 0.75);
+ -webkit-backdrop-filter: blur(10px);
+ backdrop-filter: blur(10px);
+ box-shadow: 0 0 20px var(--accent-glow);
+ /* border applied via utility classes in templates */
+}
+
+/* Border utilities for frosted-bar component */
+
+.frosted-bar.border-b,
+ .frosted-bar.border-t {
+ border-color: var(--border);
+}
+
+/* Button component styles */
+
+.btn {
+ display: inline-flex;
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0.25rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ font-weight: 700;
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+ background-color: var(--accent);
+ color: #ffffff;
+ border: none;
+ outline: none;
+}
+
+.btn:hover:not(:disabled) {
+ opacity: 0.85;
+ transform: translateY(-1px);
+}
+
+.btn:focus-visible {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+ --tw-ring-offset-width: 2px;
+ ring-color: var(--accent);
+ ring-offset-color: var(--bg);
+}
+
+.btn:active:not(:disabled) {
+ transform: translateY(0);
+ opacity: 0.75;
+}
+
+.btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Button variants */
+
+.btn-primary {
+ background-color: var(--accent);
+ color: #ffffff;
+}
+
+.btn-primary:hover:not(:disabled) {
+ background-color: var(--accent);
+}
+
+.btn-secondary {
+ background-color: var(--accent2);
+ color: var(--bg);
+ font-weight: 600;
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background-color: var(--accent2);
+}
+
+.btn-outline {
+ background-color: transparent;
+ color: var(--accent);
+ border: 2px solid var(--accent);
+}
+
+.btn-outline:hover:not(:disabled) {
+ background-color: var(--accent);
+ color: #ffffff;
+}
+
+/* Button sizes */
+
+.btn-sm {
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.btn-lg {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ padding-top: 0.75rem;
+ padding-bottom: 0.75rem;
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+}
+
+/* Icon button (for icons without text) */
+
+/* Force Feather icons to match size */
+
+/* Sidebar widget — no box, no border */
+
+.sidebar-widget {
+ margin-bottom: 1.5rem;
+}
+
+/* Sidebar widget title — bash comment style */
+
+.sidebar-widget-label {
+ font-family: var(--font-mono, monospace);
+ font-size: 1rem;
+ font-weight: bold;
+ color: var(--accent);
+ letter-spacing: 0.08em;
+ margin-bottom: 0.5rem;
+}
+
+/* Sidebar separator */
+
+.sidebar-hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin-bottom: 1.5rem;
+}
+
+/* =====================
+ Tag Cloud Component
+ ===================== */
+
+.tag-cloud {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 0.75rem;
+ align-items: baseline;
+ overflow: visible;
+}
+
+.tag-cloud-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0.25rem 0.625rem;
+ border: 1px solid var(--border);
+ border-radius: 0.25rem;
+ font-family: var(--font-mono, 'JetBrains Mono', monospace);
+ color: var(--text-dim);
+ text-decoration: none;
+ background-color: var(--bg2);
+ transition: border-color 150ms ease-out, color 150ms ease-out, background-color 150ms ease-out, opacity 150ms ease-out;
+ white-space: nowrap;
+ line-height: 1.4;
+}
+
+.tag-cloud-link:hover {
+ border-color: rgba(var(--accent-rgb), 0.5);
+ color: var(--accent);
+ background-color: rgba(var(--accent-rgb), 0.1);
+}
+
+.tag-cloud-link:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ border-radius: 0.25rem;
+}
+
+.tag-cloud-count {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0 0.375rem;
+ border-radius: 9999px;
+ font-size: 0.65em;
+ font-weight: 600;
+ background-color: rgba(var(--accent-rgb), 0.12);
+ color: var(--accent);
+ line-height: 1.6;
+ min-width: 1.2em;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .tag-cloud-link {
+ transition: none;
+ }
+}
+
+.share-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 50px);
+ justify-content: space-evenly;
+ justify-items: center;
+ align-content: space-evenly;
+ align-items: center;
+}
+
+.btn-share {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 50px;
+ height: 50px;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--text-dim);
+ cursor: pointer;
+ transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
+ text-decoration: none;
+}
+
+.btn-share svg,
+ .btn-share i,
+ .btn-share [data-feather] {
+ width: 22px !important;
+ height: 22px !important;
+ flex-shrink: 0;
+}
+
+.btn-share:hover {
+ color: var(--accent);
+ border-color: rgba(var(--accent-rgb), 0.5);
+ background: rgba(var(--accent-rgb), 0.06);
+ box-shadow: 0 0 10px var(--accent-glow);
+}
+
+.btn-share:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ color: var(--accent);
+ border-color: var(--accent);
+}
+
+.btn-share--copied {
+ color: var(--accent2);
+ border-color: var(--accent2);
+ background: var(--surface);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .btn-share {
+ transition: none;
+ }
+}
+
+/* Badge base style */
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+ border-radius: 0.25rem;
+ padding-left: 0.625rem;
+ padding-right: 0.625rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 600;
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+ border: 1px solid;
+}
+
+/* Article type badge styles */
+
+/* Legacy type-* classes for compatibility (with badge styling) */
+
+/* Card component */
+
+.card {
+ overflow: hidden;
+ border-radius: 0.5rem;
+ border-width: 1px;
+ border-color: var(--border);
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+article.card.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.border.border-border\/30.card.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.border.border-border\/30.rounded-lg.card.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.card {
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 0 30px var(--accent-glow);
+}
+
+.card-image {
+ aspect-ratio: 16 / 9;
+ width: 100%;
+ -o-object-fit: cover;
+ object-fit: cover;
+}
+
+.card-body > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
+}
+
+.card-body {
+ padding: 1.25rem;
+}
+
+@media (min-width: 768px) {
+ .card-body {
+ padding: 1.5rem;
+ }
+}
+
+/* =====================
+ Timeline Layout
+ ===================== */
+
+.timeline {
+ position: relative;
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 64rem;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+}
+
+/* Vertical spine */
+
+.timeline::before {
+ content: '';
+ position: absolute;
+ top: 0px;
+ bottom: 0px;
+ /* Mobile: fixed 20px from container left edge */
+ left: 20px;
+ width: 2px;
+ background: linear-gradient(to bottom, var(--accent), var(--accent2, var(--accent)));
+ opacity: 0.7;
+}
+
+@media (min-width: 768px) {
+ .timeline::before {
+ left: 50%;
+ transform: translateX(-50%);
+ }
+}
+
+/* Each timeline row — block, full width, positioned context for abs children */
+
+.timeline-item {
+ position: relative;
+ margin-bottom: 2.5rem;
+ /*
+ Mobile geometry (.timeline has px-4 = 16px padding):
+ spine left = 20px from <ol> border → 20-16 = 4px from item's containing block.
+ We want card to start ~8px right of spine right edge (22px from <ol> = 6px from block).
+ Use margin-left: 30px so card starts at 16+30=46px from <ol>, 24px right of spine right.
+ abs children: spine left relative to item = 4 - 30 = -26px.
+ */
+ margin-left: 30px;
+}
+
+@media (min-width: 768px) {
+ .timeline-item {
+ margin-left: 0;
+ }
+}
+
+/* ---- Connector line ---- */
+
+.timeline-connector {
+ position: absolute;
+ top: 20px;
+ height: 2px;
+ /*
+ Mobile: .timeline px-4(16px) + item margin-left(30px) = 46px from <ol> border.
+ Spine right edge = 22px from <ol> border = 22-46 = -24px from item left.
+ Connector: left=-24px, width=24px → touches spine right and card left edge.
+ */
+ left: -24px;
+ width: 24px;
+}
+
+@media (min-width: 768px) {
+ /*
+ spine center = 50% of item. Spine is 2px: left edge at 50%-1px, right at 50%+1px.
+ Cards occupy [0 .. 50%-24px] (left) or [50%+24px .. 100%] (right).
+ Left connector: left=50%-24px, width=23px → touches card right edge and spine left.
+ Right connector: left=50%+1px, width=23px → touches spine right and card left.
+ */
+
+ .timeline-item--left .timeline-connector {
+ left: calc(50% - 24px);
+ width: 23px;
+ }
+
+ .timeline-item--right .timeline-connector {
+ left: calc(50% + 1px);
+ width: 23px;
+ }
+}
+
+/* ---- Node on spine ---- */
+
+.timeline-node {
+ position: absolute;
+ z-index: 10;
+ border-radius: 9999px;
+ top: 14px;
+ /*
+ Mobile: spine center = 21px from <ol> border = 21-46 = -25px from item left.
+ Node (10px wide): left = -25 - 5 = -30px.
+ */
+ left: -30px;
+ width: 10px;
+ height: 10px;
+ border: 2px solid var(--bg);
+}
+
+@media (min-width: 768px) {
+ .timeline-node {
+ width: 12px;
+ height: 12px;
+ top: 14px;
+ /* Desktop: spine center = 50% of item. Node center on spine: left = 50% - 6px */
+ left: calc(50% - 6px);
+ }
+}
+
+/* ---- Card wrapper ---- */
+
+.timeline-card {
+ display: flex;
+ overflow: hidden;
+ border-radius: 0.5rem;
+ border-width: 1px;
+ background-color: var(--surface);
+}
+
+article.timeline-card.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.border.border-border\/30.timeline-card.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.border.border-border\/30.rounded-lg.timeline-card.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.timeline-card {
+ /* Mobile: column (thumb on top) */
+ flex-direction: column;
+ width: 100%;
+ transition: box-shadow 0.2s, transform 0.2s;
+}
+
+.timeline-card:hover {
+ transform: translateY(-2px);
+}
+
+@media (min-width: 768px) {
+ /* Desktop: push card into left or right half via item padding */
+
+ .timeline-item--left {
+ padding-right: calc(50% + 24px);
+ }
+
+ .timeline-item--right {
+ padding-left: calc(50% + 24px);
+ }
+
+ /* Left card: thumb outer-left, body inner-right */
+
+ .timeline-item--left .timeline-card {
+ flex-direction: row;
+ }
+
+ /* Right card: body inner-left, thumb outer-right */
+
+ .timeline-item--right .timeline-card {
+ flex-direction: row-reverse;
+ }
+}
+
+/* ---- Thumbnail panel ---- */
+
+.timeline-thumb {
+ flex-shrink: 0;
+ overflow: hidden;
+}
+
+article.border.border-border\/30.rounded-lg.timeline-thumb.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.timeline-thumb {
+ /* Mobile: full-width banner */
+ width: 100%;
+ height: 90px;
+}
+
+@media (min-width: 768px) {
+ .timeline-thumb {
+ /* 3:2 landscape: wider than tall */
+ width: 40%;
+ height: auto;
+ aspect-ratio: 3 / 2;
+ }
+}
+
+.timeline-thumb img {
+ height: 100%;
+ width: 100%;
+ -o-object-fit: cover;
+ object-fit: cover;
+ transition: transform 0.2s;
+}
+
+.timeline-card:hover .timeline-thumb img {
+ transform: scale(1.03);
+}
+
+/* ---- Text panel ---- */
+
+.timeline-body {
+ display: flex;
+ flex: 1 1 0%;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 1rem;
+}
+
+/* ---- Meta row (TYPE · date) ---- */
+
+.timeline-meta {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+}
+
+.timeline-meta-sep {
+ color: var(--border);
+}
+
+.timeline-date {
+ text-transform: none;
+ letter-spacing: 0em;
+ color: var(--text-dim);
+}
+
+/* ---- Title ---- */
+
+.timeline-title {
+ font-size: 1rem;
+ line-height: 1.5rem;
+ font-weight: 600;
+ line-height: 1.375;
+}
+
+.timeline-title a {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.timeline-title a:hover {
+ color: var(--accent);
+}
+
+/* ---- Excerpt ---- */
+
+.timeline-excerpt {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ line-height: 1.625;
+ color: var(--text-dim);
+}
+
+/* ---- Pinned badge ---- */
+
+.timeline-pinned {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ border-radius: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ padding-top: 0.125rem;
+ padding-bottom: 0.125rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ font-weight: 600;
+}
+
+/* ---- Type-color variants (node, connector, card) ---- */
+
+/* Tech (purple) */
+
+/* Life (amber) */
+
+/* Quote (green) */
+
+/* Link (cyan) */
+
+/* Photo (pink) */
+
+/* ---- Timeline lazy-reveal (scroll-triggered) ---- */
+
+.js-lazy-timeline > .timeline-item {
+ opacity: 0;
+ transform: translateX(-18px);
+ transition: opacity 320ms ease-out, transform 320ms ease-out;
+}
+
+.js-lazy-timeline > .timeline-item.is-visible {
+ opacity: 1;
+ transform: translateX(0);
+}
+
+/* Header navigation styling */
+
+.header {
+ position: fixed;
+ top: 0px;
+ left: 0px;
+ right: 0px;
+ z-index: 40;
+}
+
+/* Mobile menu overlay */
+
+.menu-overlay.active {
+ visibility: visible;
+ opacity: 1;
+}
+
+/* Breadcrumb navigation */
+
+.breadcrumb {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ color: var(--text-dim);
+}
+
+.breadcrumb a {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.breadcrumb a:hover {
+ color: var(--accent);
+}
+
+.breadcrumb-separator {
+ opacity: 0.5;
+}
+
+/* Article metadata styling (with icons) */
+
+/* Hero typography with fluid sizing */
+
+/* ---- Article prev/next navigation ---- */
+
+.article-nav {
+ border-color: var(--border);
+}
+
+
+ .frosted-bar.article-nav {
+ border-color: var(--border);
+}
+
+.article-nav {
+ border-top-width: 1px;
+ border-color: var(--border);
+ padding-top: 1.5rem;
+}
+
+.article-nav-prompt {
+ margin-bottom: 0.5rem;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ color: var(--accent);
+}
+
+.article-nav-links {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+@media (min-width: 768px) {
+ .article-nav-links {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0px;
+ }
+}
+
+.article-nav-link {
+ color: var(--text);
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.article-nav-link:hover {
+ color: var(--accent);
+}
+
+.article-nav-placeholder {
+ color: var(--text-dim);
+ opacity: 0.4;
+}
+
+/* ---- Footer badge variants ---- */
+
+.badge-footer-accent {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+ border-radius: 0.25rem;
+ padding-left: 0.625rem;
+ padding-right: 0.625rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ font-weight: 600;
+ border: 1px solid rgba(168, 85, 247, 0.35);
+ background: rgba(168, 85, 247, 0.1);
+ color: var(--accent);
+}
+
+.badge-footer-accent2 {
+ display: inline-flex;
+ align-items: center;
+ white-space: nowrap;
+ border-radius: 0.25rem;
+ padding-left: 0.625rem;
+ padding-right: 0.625rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ font-family: JetBrains Mono, monospace;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ font-weight: 600;
+ border: 1px solid rgba(0, 255, 136, 0.35);
+ background: rgba(0, 255, 136, 0.1);
+ color: var(--accent2);
+}
+
+/* Back to top button */
+
+.back-to-top {
+ position: fixed;
+ bottom: 1.5rem;
+ right: 1.5rem;
+ z-index: 40;
+ display: flex;
+ height: 2.75rem;
+ width: 2.75rem;
+ align-items: center;
+ justify-content: center;
+ border-radius: 9999px;
+ background: var(--accent);
+ box-shadow: 0 0 12px rgba(var(--accent-rgb), 0.4);
+ transition: background 200ms ease, box-shadow 200ms ease;
+ color: #fff;
+}
+
+.back-to-top:hover {
+ background: var(--accent);
+ filter: brightness(0.85);
+ box-shadow: 0 0 20px var(--accent);
+}
+
+.back-to-top:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border-width: 0;
+}
+
+.pointer-events-none {
+ pointer-events: none;
+}
+
+.visible {
+ visibility: visible;
+}
+
+.invisible {
+ visibility: hidden;
+}
+
+.static {
+ position: static;
+}
+
+.fixed {
+ position: fixed;
+}
+
+.absolute {
+ position: absolute;
+}
+
+.relative {
+ position: relative;
+}
+
+.sticky {
+ position: sticky;
+}
+
+.inset-0 {
+ inset: 0px;
+}
+
+.bottom-0 {
+ bottom: 0px;
+}
+
+.left-0 {
+ left: 0px;
+}
+
+.right-0 {
+ right: 0px;
+}
+
+.right-3 {
+ right: 0.75rem;
+}
+
+.right-4 {
+ right: 1rem;
+}
+
+.top-0 {
+ top: 0px;
+}
+
+.top-3 {
+ top: 0.75rem;
+}
+
+.top-4 {
+ top: 1rem;
+}
+
+.z-10 {
+ z-index: 10;
+}
+
+.z-20 {
+ z-index: 20;
+}
+
+.z-40 {
+ z-index: 40;
+}
+
+.z-50 {
+ z-index: 50;
+}
+
+.order-last {
+ order: 9999;
+}
+
+.mx-4 {
+ margin-left: 1rem;
+ margin-right: 1rem;
+}
+
+.mx-auto {
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.my-12 {
+ margin-top: 3rem;
+ margin-bottom: 3rem;
+}
+
+.my-6 {
+ margin-top: 1.5rem;
+ margin-bottom: 1.5rem;
+}
+
+.my-8 {
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+}
+
+.mb-1 {
+ margin-bottom: 0.25rem;
+}
+
+.mb-12 {
+ margin-bottom: 3rem;
+}
+
+.mb-2 {
+ margin-bottom: 0.5rem;
+}
+
+.mb-3 {
+ margin-bottom: 0.75rem;
+}
+
+.mb-4 {
+ margin-bottom: 1rem;
+}
+
+.mb-6 {
+ margin-bottom: 1.5rem;
+}
+
+.mb-8 {
+ margin-bottom: 2rem;
+}
+
+.ml-1 {
+ margin-left: 0.25rem;
+}
+
+.ml-2 {
+ margin-left: 0.5rem;
+}
+
+.mt-0\.5 {
+ margin-top: 0.125rem;
+}
+
+.mt-1 {
+ margin-top: 0.25rem;
+}
+
+.mt-12 {
+ margin-top: 3rem;
+}
+
+.mt-16 {
+ margin-top: 4rem;
+}
+
+.mt-2 {
+ margin-top: 0.5rem;
+}
+
+.mt-20 {
+ margin-top: 5rem;
+}
+
+.mt-3 {
+ margin-top: 0.75rem;
+}
+
+.mt-4 {
+ margin-top: 1rem;
+}
+
+.mt-8 {
+ margin-top: 2rem;
+}
+
+.mt-auto {
+ margin-top: auto;
+}
+
+.line-clamp-1 {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1;
+}
+
+.line-clamp-3 {
+ overflow: hidden;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+}
+
+.block {
+ display: block;
+}
+
+.inline {
+ display: inline;
+}
+
+.flex {
+ display: flex;
+}
+
+.inline-flex {
+ display: inline-flex;
+}
+
+.grid {
+ display: grid;
+}
+
+.hidden {
+ display: none;
+}
+
+.aspect-video {
+ aspect-ratio: 16 / 9;
+}
+
+.h-1 {
+ height: 0.25rem;
+}
+
+.h-32 {
+ height: 8rem;
+}
+
+.h-4 {
+ height: 1rem;
+}
+
+.h-48 {
+ height: 12rem;
+}
+
+.h-5 {
+ height: 1.25rem;
+}
+
+.h-auto {
+ height: auto;
+}
+
+.h-screen {
+ height: 100vh;
+}
+
+.max-h-96 {
+ max-height: 24rem;
+}
+
+.min-h-\[calc\(100vh-200px\)\] {
+ min-height: calc(100vh - 200px);
+}
+
+.min-h-screen {
+ min-height: 100vh;
+}
+
+.w-1 {
+ width: 0.25rem;
+}
+
+.w-20 {
+ width: 5rem;
+}
+
+.w-32 {
+ width: 8rem;
+}
+
+.w-4 {
+ width: 1rem;
+}
+
+.w-5 {
+ width: 1.25rem;
+}
+
+.w-full {
+ width: 100%;
+}
+
+.min-w-0 {
+ min-width: 0px;
+}
+
+.max-w-2xl {
+ max-width: 42rem;
+}
+
+.max-w-3xl {
+ max-width: 48rem;
+}
+
+.max-w-4xl {
+ max-width: 56rem;
+}
+
+.max-w-5xl {
+ max-width: 64rem;
+}
+
+.max-w-7xl {
+ max-width: 80rem;
+}
+
+.max-w-lg {
+ max-width: 32rem;
+}
+
+.max-w-md {
+ max-width: 28rem;
+}
+
+.max-w-none {
+ max-width: none;
+}
+
+.max-w-sm {
+ max-width: 24rem;
+}
+
+.flex-1 {
+ flex: 1 1 0%;
+}
+
+.flex-shrink-0 {
+ flex-shrink: 0;
+}
+
+.shrink-0 {
+ flex-shrink: 0;
+}
+
+.translate-x-0 {
+ --tw-translate-x: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.translate-x-full {
+ --tw-translate-x: 100%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.translate-y-0 {
+ --tw-translate-y: 0px;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.translate-y-4 {
+ --tw-translate-y: 1rem;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.transform {
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.cursor-pointer {
+ cursor: pointer;
+}
+
+.resize-none {
+ resize: none;
+}
+
+.resize {
+ resize: both;
+}
+
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.flex-wrap {
+ flex-wrap: wrap;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.justify-between {
+ justify-content: space-between;
+}
+
+.gap-1 {
+ gap: 0.25rem;
+}
+
+.gap-1\.5 {
+ gap: 0.375rem;
+}
+
+.gap-2 {
+ gap: 0.5rem;
+}
+
+.gap-3 {
+ gap: 0.75rem;
+}
+
+.gap-4 {
+ gap: 1rem;
+}
+
+.gap-6 {
+ gap: 1.5rem;
+}
+
+.gap-8 {
+ gap: 2rem;
+}
+
+.space-y-1 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.25rem * var(--tw-space-y-reverse));
+}
+
+.space-y-2 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
+}
+
+.space-y-3 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
+}
+
+.space-y-4 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1rem * var(--tw-space-y-reverse));
+}
+
+.space-y-6 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
+}
+
+.space-y-8 > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(2rem * var(--tw-space-y-reverse));
+}
+
+.overflow-hidden {
+ overflow: hidden;
+}
+
+.overflow-y-auto {
+ overflow-y: auto;
+}
+
+.truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.whitespace-nowrap {
+ white-space: nowrap;
+}
+
+.rounded {
+ border-radius: 0.25rem;
+}
+
+.rounded-full {
+ border-radius: 9999px;
+}
+
+.rounded-lg {
+ border-radius: 0.5rem;
+}
+
+.rounded-b-lg {
+ border-bottom-right-radius: 0.5rem;
+ border-bottom-left-radius: 0.5rem;
+}
+
+.border {
+ border-width: 1px;
+}
+
+.border-2 {
+ border-width: 2px;
+}
+
+.border-4 {
+ border-width: 4px;
+}
+
+.border-b {
+ border-bottom-width: 1px;
+}
+
+.border-l {
+ border-left-width: 1px;
+}
+
+.border-l-2 {
+ border-left-width: 2px;
+}
+
+.border-l-4 {
+ border-left-width: 4px;
+}
+
+.border-t {
+ border-top-width: 1px;
+}
+
+.border-\[--type-quote\] {
+ border-color: var(--type-quote);
+}
+
+.border-accent {
+ border-color: var(--accent);
+}
+
+.border-border {
+ border-color: var(--border);
+}
+
+.bg-accent {
+ background-color: var(--accent);
+}
+
+.bg-bg {
+ background-color: var(--bg);
+}
+
+.bg-black\/50 {
+ background-color: rgb(0 0 0 / 0.5);
+}
+
+.bg-surface {
+ background-color: var(--surface);
+}
+
+.object-cover {
+ -o-object-fit: cover;
+ object-fit: cover;
+}
+
+.p-2 {
+ padding: 0.5rem;
+}
+
+.p-3 {
+ padding: 0.75rem;
+}
+
+.p-4 {
+ padding: 1rem;
+}
+
+.p-6 {
+ padding: 1.5rem;
+}
+
+.p-8 {
+ padding: 2rem;
+}
+
+.px-1 {
+ padding-left: 0.25rem;
+ padding-right: 0.25rem;
+}
+
+.px-2 {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+
+.px-2\.5 {
+ padding-left: 0.625rem;
+ padding-right: 0.625rem;
+}
+
+.px-3 {
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+}
+
+.px-4 {
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.px-6 {
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+}
+
+.py-0\.5 {
+ padding-top: 0.125rem;
+ padding-bottom: 0.125rem;
+}
+
+.py-1 {
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+}
+
+.py-1\.5 {
+ padding-top: 0.375rem;
+ padding-bottom: 0.375rem;
+}
+
+.py-12 {
+ padding-top: 3rem;
+ padding-bottom: 3rem;
+}
+
+.py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.py-3 {
+ padding-top: 0.75rem;
+ padding-bottom: 0.75rem;
+}
+
+.py-4 {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+.py-8 {
+ padding-top: 2rem;
+ padding-bottom: 2rem;
+}
+
+.pb-8 {
+ padding-bottom: 2rem;
+}
+
+.pl-3 {
+ padding-left: 0.75rem;
+}
+
+.pl-6 {
+ padding-left: 1.5rem;
+}
+
+.pt-2 {
+ padding-top: 0.5rem;
+}
+
+.pt-8 {
+ padding-top: 2rem;
+}
+
+.text-left {
+ text-align: left;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.font-mono {
+ font-family: JetBrains Mono, monospace;
+}
+
+.font-oxanium {
+ font-family: Oxanium, monospace;
+}
+
+.text-2xl {
+ font-size: 1.5rem;
+ line-height: 2rem;
+}
+
+.text-3xl {
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+}
+
+.text-4xl {
+ font-size: 2.25rem;
+ line-height: 2.5rem;
+}
+
+.text-5xl {
+ font-size: 3rem;
+ line-height: 1;
+}
+
+.text-7xl {
+ font-size: 4.5rem;
+ line-height: 1;
+}
+
+.text-lg {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+}
+
+.text-sm {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+}
+
+.text-xs {
+ font-size: 0.75rem;
+ line-height: 1rem;
+}
+
+.font-bold {
+ font-weight: 700;
+}
+
+.font-medium {
+ font-weight: 500;
+}
+
+.font-semibold {
+ font-weight: 600;
+}
+
+.italic {
+ font-style: italic;
+}
+
+.not-italic {
+ font-style: normal;
+}
+
+.leading-relaxed {
+ line-height: 1.625;
+}
+
+.leading-snug {
+ line-height: 1.375;
+}
+
+.text-accent {
+ color: var(--accent);
+}
+
+.text-accent2 {
+ color: var(--accent2);
+}
+
+.text-bg {
+ color: var(--bg);
+}
+
+.text-text {
+ color: var(--text);
+}
+
+.text-text-dim {
+ color: var(--text-dim);
+}
+
+.text-white {
+ --tw-text-opacity: 1;
+ color: rgb(255 255 255 / var(--tw-text-opacity, 1));
+}
+
+.underline {
+ text-decoration-line: underline;
+}
+
+.line-through {
+ text-decoration-line: line-through;
+}
+
+.antialiased {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.placeholder-text-dim::-moz-placeholder {
+ color: var(--text-dim);
+}
+
+.placeholder-text-dim::placeholder {
+ color: var(--text-dim);
+}
+
+.opacity-0 {
+ opacity: 0;
+}
+
+.opacity-100 {
+ opacity: 1;
+}
+
+.opacity-5 {
+ opacity: 0.05;
+}
+
+.shadow-xl {
+ --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.outline {
+ outline-style: solid;
+}
+
+.ring {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+
+.backdrop-blur {
+ --tw-backdrop-blur: blur(8px);
+ -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
+ backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
+}
+
+.transition {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.transition-all {
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.transition-colors {
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.transition-opacity {
+ transition-property: opacity;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.transition-shadow {
+ transition-property: box-shadow;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.transition-transform {
+ transition-property: transform;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.duration-100 {
+ transition-duration: 100ms;
+}
+
+.duration-200 {
+ transition-duration: 200ms;
+}
+
+.duration-300 {
+ transition-duration: 300ms;
+}
+
+.ease-in {
+ transition-timing-function: cubic-bezier(0.4, 0, 1, 1);
+}
+
+.ease-out {
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
+}
+
+/* Dark theme (default) - CSS custom properties */
+
+:root {
+ --bg: #060b10;
+ --bg2: #0c1520;
+ --bg2-rgb: 12, 21, 32;
+ --surface: #101e2d;
+ --surface-rgb: 16, 30, 45;
+ --border: #182840;
+ --accent: #a855f7;
+ --accent-rgb: 168, 85, 247;
+ --accent2: #00ff88;
+ --accent-glow: rgba(168, 85, 247, 0.12);
+ --text: #c4d6e8;
+ --text-dim: #7a9bb8;
+ --muted: #304860;
+ /* Article type colors - dark */
+ --type-tech: #a855f7;
+ --type-life: #f59e0b;
+ --type-quote: #00ff88;
+ --type-link: #38bdf8;
+ --type-photo: #ec4899;
+ /* Article type text colors - dark (all black for WCAG AA) */
+ --type-tech-text: #000000;
+ --type-life-text: #000000;
+ --type-quote-text: #000000;
+ --type-link-text: #000000;
+ --type-photo-text: #000000;
+}
+
+/* Light theme overrides */
+
+html.theme-light {
+ --bg: #ffffff;
+ --bg2: #f8f9fa;
+ --bg2-rgb: 248, 249, 250;
+ --surface: #f0f3f7;
+ --surface-rgb: 240, 243, 247;
+ --border: #d9dfe8;
+ --accent: #9333ea;
+ --accent-rgb: 147, 51, 234;
+ --accent2: #10b981;
+ --accent-glow: rgba(147, 51, 234, 0.1);
+ --text: #1f2937;
+ --text-dim: #374151;
+ --muted: #d1d5db;
+ /* Article type colors - light */
+ --type-tech: #7c3aed;
+ --type-life: #d97706;
+ --type-quote: #008f5a;
+ --type-link: #0284c7;
+ --type-photo: #be185d;
+ /* Article type text colors - light (mixed for WCAG AA) */
+ --type-tech-text: #ffffff;
+ --type-life-text: #000000;
+ --type-quote-text: #000000;
+ --type-link-text: #000000;
+ --type-photo-text: #ffffff;
+}
+
+/* No-JS fallback: prefers-color-scheme light */
+
+@media (prefers-color-scheme: light) {
+ html:not(.theme-dark) {
+ --bg: #ffffff;
+ --bg2: #f8f9fa;
+ --bg2-rgb: 248, 249, 250;
+ --surface: #f0f3f7;
+ --surface-rgb: 240, 243, 247;
+ --border: #d9dfe8;
+ --accent: #9333ea;
+ --accent-rgb: 147, 51, 234;
+ --accent2: #10b981;
+ --accent-glow: rgba(147, 51, 234, 0.1);
+ --text: #1f2937;
+ --text-dim: #374151;
+ --muted: #d1d5db;
+ --type-tech: #7c3aed;
+ --type-life: #d97706;
+ --type-quote: #008f5a;
+ --type-link: #0284c7;
+ --type-photo: #be185d;
+ --type-tech-text: #ffffff;
+ --type-life-text: #000000;
+ --type-quote-text: #000000;
+ --type-link-text: #000000;
+ --type-photo-text: #ffffff;
+ }
+}
+
+/* Theme-aware picture element for default thumbnails */
+
+html.theme-light picture img[src="/images/default_thumbnail_dark.png"] {
+ content: url('/images/default_thumbnail_light.png');
+}
+
+/* Prose overrides for light theme */
+
+html.theme-light .prose,
+html.theme-light .prose-invert {
+ color: var(--text);
+}
+
+html.theme-light .prose a,
+html.theme-light .prose-invert a {
+ color: var(--accent);
+}
+
+html.theme-light .prose strong,
+html.theme-light .prose-invert strong {
+ color: var(--text);
+}
+
+html.theme-light .prose code,
+html.theme-light .prose-invert code {
+ color: var(--accent2);
+}
+
+html.theme-light .prose pre,
+html.theme-light .prose-invert pre {
+ background-color: var(--surface);
+ color: var(--text);
+}
+
+html.theme-light .prose h1,
+html.theme-light .prose h2,
+html.theme-light .prose h3,
+html.theme-light .prose h4,
+html.theme-light .prose h5,
+html.theme-light .prose h6,
+html.theme-light .prose-invert h1,
+html.theme-light .prose-invert h2,
+html.theme-light .prose-invert h3,
+html.theme-light .prose-invert h4,
+html.theme-light .prose-invert h5,
+html.theme-light .prose-invert h6 {
+ color: var(--text);
+}
+
+html.theme-light .prose blockquote,
+html.theme-light .prose-invert blockquote {
+ color: var(--text);
+ border-left-color: var(--accent);
+}
+
+/* Responsive container utilities - mobile-first */
+
+.container {
+ max-width: 100%;
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+@media (min-width: 768px) {
+ .container {
+ max-width: 56rem;
+ padding-left: 1.5rem;
+ padding-right: 1.5rem;
+ }
+}
+
+@media (min-width: 1060px) {
+ .container {
+ max-width: 64rem;
+ padding-left: 2rem;
+ padding-right: 2rem;
+ }
+}
+
+/* Alpine.js x-cloak - hide content until Alpine initializes */
+
+[x-cloak] {
+ display: none !important;
+}
+
+/* Respect user's motion preferences */
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ }
+}
+
+/* Matrix rain canvas background */
+
+#matrix-rain {
+ position: fixed;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ z-index: 1;
+}
+
+/* Dark theme: 13% opacity (inner pages) */
+
+html.theme-dark #matrix-rain {
+ opacity: 0.13;
+}
+
+/* Light theme: 18% opacity (inner pages) */
+
+html.theme-light #matrix-rain {
+ opacity: 0.18;
+}
+
+/* Homepage: more prominent background */
+
+html.theme-dark body[data-page-kind="home"] #matrix-rain {
+ opacity: 0.28;
+}
+
+html.theme-light body[data-page-kind="home"] #matrix-rain {
+ opacity: 0.35;
+}
+
+/* Reduced motion: hide canvas entirely */
+
+@media (prefers-reduced-motion: reduce) {
+ #matrix-rain {
+ display: none;
+ }
+}
+
+/* Content grid background — blocks rain under text, visible in gutters (single pages only) */
+
+.content-grid {
+ position: relative;
+ z-index: 10;
+ background-color: var(--bg);
+ padding: 2px;
+ border: 1px solid var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+@media (min-width: 768px) {
+ .content-grid {
+ padding: 2rem;
+ }
+}
+
+/* Article list items — soft glow effect */
+
+article.border.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+/* ============================================
+ FORM COMPONENTS (Week 4)
+ ============================================ */
+
+/* Form input base styles */
+
+.form-input,
+.form-textarea,
+.form-select {
+ width: 100%;
+ border-radius: 0.25rem;
+ border-width: 1px;
+ border-color: var(--border);
+ background-color: var(--bg2);
+ padding-left: 1rem;
+ padding-right: 1rem;
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ font-family: IBM Plex Sans, sans-serif;
+ color: var(--text);
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+}
+
+article.form-input.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg,article
+.form-textarea.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg,article
+.form-select.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+/* Input placeholder styling */
+
+.form-input::-moz-placeholder, .form-textarea::-moz-placeholder {
+ color: var(--text-dim);
+ opacity: 0.7;
+}
+
+.form-input::placeholder,
+.form-textarea::placeholder {
+ color: var(--text-dim);
+ opacity: 0.7;
+}
+
+/* Input focus state */
+
+.form-input:focus,
+.form-textarea:focus,
+.form-select:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+ --tw-ring-color: var(--accent);
+ --tw-ring-offset-width: 2px;
+ ring-offset-color: var(--bg);
+ border-color: var(--accent);
+}
+
+/* Input invalid/error state */
+
+.form-input:invalid,
+.form-textarea:invalid,
+.form-select:invalid,
+.form-input.error,
+.form-textarea.error,
+.form-select.error {
+ --tw-border-opacity: 1;
+ border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1));
+}
+
+.form-input:invalid:focus,
+.form-textarea:invalid:focus,
+.form-select:invalid:focus {
+ --tw-border-opacity: 1;
+ border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity, 1));
+ ring-offset-color: var(--bg);
+}
+
+/* Input disabled state */
+
+.form-input:disabled,
+.form-textarea:disabled,
+.form-select:disabled {
+ cursor: not-allowed;
+ background-color: var(--muted);
+ opacity: 0.5;
+}
+
+/* Textarea specific */
+
+.form-textarea {
+ min-height: 8rem;
+ resize: vertical;
+}
+
+.form-textarea.auto-expand {
+ resize: none;
+ overflow-y: hidden;
+}
+
+/* Select dropdown */
+
+.form-select {
+ cursor: pointer;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23a855f7' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.75rem center;
+ padding-right: 2.5rem;
+}
+
+html.theme-light .form-select {
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%239333ea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
+}
+
+/* Checkbox and radio button base */
+
+.form-checkbox,
+.form-radio {
+ height: 1.25rem;
+ width: 1.25rem;
+ cursor: pointer;
+ accent-color: var(--accent);
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ border: 2px solid var(--border);
+ border-radius: 0.375rem;
+ flex-shrink: 0;
+}
+
+.form-radio {
+ border-radius: 50%;
+}
+
+/* Checkbox/radio focus state */
+
+.form-checkbox:focus-visible,
+.form-radio:focus-visible {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+ --tw-ring-color: var(--accent);
+ --tw-ring-offset-width: 2px;
+ ring-offset-color: var(--bg);
+}
+
+/* Checkbox/radio checked state */
+
+.form-checkbox:checked,
+.form-radio:checked {
+ background-color: var(--accent);
+ border-color: var(--accent);
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: 100%;
+}
+
+.form-radio:checked {
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3.5'/%3E%3C/svg%3E");
+}
+
+/* Checkbox/radio disabled state */
+
+.form-checkbox:disabled,
+.form-radio:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ border-color: var(--muted);
+}
+
+/* Form group layout */
+
+.form-group > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
+}
+
+.form-group label {
+ display: block;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ font-weight: 600;
+ color: var(--text);
+}
+
+.form-group.required label::after {
+ content: ' *';
+ color: #ef4444;
+}
+
+.form-group-input {
+ position: relative;
+}
+
+.form-group-help {
+ font-size: 0.75rem;
+ line-height: 1rem;
+ color: var(--text-dim);
+}
+
+.form-group.error .form-group-help {
+ --tw-text-opacity: 1;
+ color: rgb(239 68 68 / var(--tw-text-opacity, 1));
+}
+
+.form-error {
+ margin-top: 0.25rem;
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ --tw-text-opacity: 1;
+ color: rgb(239 68 68 / var(--tw-text-opacity, 1));
+}
+
+/* Form layout utilities */
+
+.form-row {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+@media (min-width: 768px) {
+ .form-row {
+ flex-direction: row;
+ }
+}
+
+.form-row > .form-group {
+ flex: 1 1 0%;
+}
+
+.form-inline {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 1rem;
+}
+
+@media (min-width: 640px) {
+ .form-inline {
+ flex-direction: row;
+ }
+}
+
+.form-inline .form-group {
+ flex: 1 1 0%;
+}
+
+.form-inline .btn {
+ height: 2.5rem;
+}
+
+/* Character count indicator */
+
+.form-char-count {
+ text-align: right;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ color: var(--text-dim);
+}
+
+.form-char-count.warning {
+ --tw-text-opacity: 1;
+ color: rgb(245 158 11 / var(--tw-text-opacity, 1));
+}
+
+.form-char-count.error {
+ --tw-text-opacity: 1;
+ color: rgb(239 68 68 / var(--tw-text-opacity, 1));
+}
+
+/* ============================================
+ FOCUS MANAGEMENT (Week 5)
+ ============================================ */
+
+/* Enhanced :focus-visible with accent color styling */
+
+:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+/* Respect motion preferences for focus indicator */
+
+@media (prefers-reduced-motion: reduce) {
+ :focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+/* Button and link hover/focus transitions */
+
+button,
+a.btn,
+.btn {
+ transition: all 150ms ease-out;
+}
+
+button:hover,
+a.btn:hover,
+.btn:hover {
+ opacity: 0.8;
+ transform: translateY(-1px);
+}
+
+button:active,
+a.btn:active,
+.btn:active {
+ transform: translateY(0);
+}
+
+/* Form input focus transitions with glow effect */
+
+input,
+textarea,
+select {
+ transition: all 200ms ease-out;
+}
+
+input:focus,
+textarea:focus,
+select:focus,
+input:focus-visible,
+textarea:focus-visible,
+select:focus-visible {
+ box-shadow: 0 0 0 3px rgba(var(--accent-rgb), 0.1);
+}
+
+/* ============================================
+ MODAL COMPONENTS (Week 4)
+ ============================================ */
+
+/* Modal backdrop */
+
+.modal-backdrop {
+ visibility: hidden;
+ position: fixed;
+ inset: 0px;
+ z-index: 40;
+ background-color: rgb(0 0 0 / 0.5);
+ opacity: 0;
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 300ms;
+ -webkit-backdrop-filter: blur(2px);
+ backdrop-filter: blur(2px);
+}
+
+.modal-backdrop.active {
+ visibility: visible;
+ opacity: 1;
+}
+
+/* Modal container */
+
+.modal {
+ visibility: hidden;
+ position: fixed;
+ inset: 0px;
+ z-index: 50;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 1rem;
+ opacity: 0;
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 300ms;
+ pointer-events: none;
+}
+
+.modal.active {
+ visibility: visible;
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.modal.active .modal-backdrop {
+ visibility: visible;
+ opacity: 1;
+ pointer-events: auto;
+}
+
+/* Modal content box */
+
+.modal-content {
+ display: flex;
+ max-height: 90vh;
+ width: 100%;
+ max-width: 32rem;
+ flex-direction: column;
+ border-radius: 0.5rem;
+ border-width: 1px;
+ border-color: var(--border);
+ background-color: var(--bg2);
+ --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
+ --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+article.modal-content.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.border.border-border\/30.modal-content.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.modal-content {
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ animation: modalSlideUp 0.3s ease-out;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes slideUp {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes modalSlideUp {
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* Modal header */
+
+.modal-header {
+ border-color: var(--border);
+}
+
+.frosted-bar.modal-header {
+ border-color: var(--border);
+}
+
+.modal-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+ border-bottom-width: 1px;
+ border-color: var(--border);
+ padding: 1.5rem;
+}
+
+.modal-title {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+ font-weight: 700;
+ color: var(--text);
+}
+
+.modal-close {
+ display: flex;
+ height: 2rem;
+ width: 2rem;
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0.25rem;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.modal-close:hover {
+ background-color: var(--surface);
+}
+
+.modal-close::before,
+.modal-close::after {
+ content: '';
+ position: absolute;
+ height: 0.125rem;
+ width: 1.25rem;
+ background-color: var(--text-dim);
+}
+
+.modal-close::before {
+ transform: rotate(45deg);
+}
+
+.modal-close::after {
+ transform: rotate(-45deg);
+}
+
+.modal-close:hover::before,
+.modal-close:hover::after {
+ background-color: var(--accent);
+}
+
+/* Modal body */
+
+.modal-body {
+ flex: 1 1 0%;
+}
+
+.modal-body > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(1rem * var(--tw-space-y-reverse));
+}
+
+.modal-body {
+ overflow-y: auto;
+ padding: 1.5rem;
+}
+
+/* Modal footer */
+
+.modal-footer {
+ border-color: var(--border);
+}
+
+
+ .frosted-bar.modal-footer {
+ border-color: var(--border);
+}
+
+.modal-footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 0.75rem;
+ border-top-width: 1px;
+ border-color: var(--border);
+ padding: 1.5rem;
+}
+
+/* Modal sizes */
+
+.modal-content.modal-sm {
+ max-width: 24rem;
+}
+
+.modal-content.modal-md {
+ max-width: 28rem;
+}
+
+.modal-content.modal-lg {
+ max-width: 42rem;
+}
+
+/* Modal variants */
+
+.modal-content.modal-alert {
+ max-width: 24rem;
+}
+
+.modal-content.modal-confirm {
+ max-width: 24rem;
+}
+
+/* Modal button styling */
+
+.modal-footer .btn {
+ min-width: 100px;
+}
+
+/* ============================================
+ INTERACTIVE PATTERNS (Week 4)
+ ============================================ */
+
+/* Loading spinner */
+
+.spinner {
+ display: inline-block;
+ width: 1rem;
+ height: 1rem;
+ border: 2px solid var(--border);
+ border-top-color: var(--accent);
+ border-radius: 50%;
+ animation: spin 0.6s linear infinite;
+}
+
+@keyframes spin {
+ from {
+ transform: rotate(0deg);
+ }
+
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.spinner-sm {
+ width: 0.75rem;
+ height: 0.75rem;
+ border-width: 1.5px;
+}
+
+.spinner-lg {
+ width: 1.5rem;
+ height: 1.5rem;
+ border-width: 3px;
+}
+
+/* Button with spinner */
+
+.btn:disabled .spinner {
+ margin-right: 0.5rem;
+ display: inline-block;
+}
+
+/* Toast notification container */
+
+.toast-container {
+ position: fixed;
+ bottom: 0px;
+ right: 0px;
+ z-index: 50;
+ max-width: 24rem;
+}
+
+.toast-container > :not([hidden]) ~ :not([hidden]) {
+ --tw-space-y-reverse: 0;
+ margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
+ margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
+}
+
+.toast-container {
+ padding: 1rem;
+}
+
+@media (max-width: 640px) {
+ .toast-container {
+ left: 0px;
+ right: 0px;
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+}
+
+/* Toast base */
+
+.toast {
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+ border-radius: 0.5rem;
+ border-width: 1px;
+ border-color: var(--border);
+ padding: 1rem;
+ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+article.border.border-border\/30.toast.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+article.toast.border-border\/30.rounded-lg.overflow-hidden.group.bg-bg {
+ border-color: var(--border);
+ box-shadow: 0 0 20px var(--accent-glow);
+}
+
+.toast {
+ animation: slideInUp 0.3s ease-out;
+ background-color: var(--bg2);
+ color: var(--text);
+}
+
+@keyframes slideInUp {
+ from {
+ transform: translateY(100%);
+ opacity: 0;
+ }
+
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Animation Utility Classes */
+
+.animate-fade-in {
+ animation: fadeIn 300ms ease-out;
+}
+
+.animate-slide-up {
+ animation: slideUp 300ms ease-out;
+}
+
+.animate-spin-loader {
+ animation: spin 600ms linear infinite;
+}
+
+/* Toast variants */
+
+.toast-success {
+ border-color: #10b981;
+ background-color: rgba(16, 185, 129, 0.1);
+}
+
+.toast-success::before {
+ content: '✓';
+ color: #10b981;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+
+.toast-error {
+ border-color: #ef4444;
+ background-color: rgba(239, 68, 68, 0.1);
+}
+
+.toast-error::before {
+ content: '✕';
+ color: #ef4444;
+ font-weight: bold;
+ flex-shrink: 0;
+}
+
+.toast-info {
+ border-color: #3b82f6;
+ background-color: rgba(59, 130, 246, 0.1);
+}
+
+.toast-info::before {
+ content: 'ℹ';
+ color: #3b82f6;
+ flex-shrink: 0;
+}
+
+.toast-warning {
+ border-color: #f59e0b;
+ background-color: rgba(245, 158, 11, 0.1);
+}
+
+.toast-warning::before {
+ content: '⚠';
+ color: #f59e0b;
+ flex-shrink: 0;
+}
+
+.toast-close {
+ margin-left: auto;
+ display: flex;
+ height: 1.5rem;
+ width: 1.5rem;
+ flex-shrink: 0;
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0.25rem;
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.toast-close:hover {
+ background-color: var(--surface);
+}
+
+/* Tooltip */
+
+.tooltip {
+ position: relative;
+ display: inline-block;
+}
+
+.tooltip-text {
+ pointer-events: none;
+ visibility: hidden;
+ position: absolute;
+ white-space: nowrap;
+ border-radius: 0.25rem;
+ background-color: var(--bg2);
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ color: var(--text-dim);
+ opacity: 0;
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 200ms;
+ z-index: 50;
+ bottom: 125%;
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.tooltip:hover .tooltip-text {
+ visibility: visible;
+ opacity: 1;
+}
+
+.tooltip-text::after {
+ content: '';
+ position: absolute;
+ height: 0.5rem;
+ width: 0.5rem;
+ background-color: var(--bg2);
+ bottom: -4px;
+ left: 50%;
+ transform: translateX(-50%) rotate(45deg);
+}
+
+/* Tooltip directions */
+
+.tooltip-bottom .tooltip-text {
+ top: 125%;
+ bottom: auto;
+}
+
+.tooltip-bottom .tooltip-text::after {
+ top: -4px;
+ bottom: auto;
+ transform: translateX(-50%) rotate(225deg);
+}
+
+.tooltip-left .tooltip-text {
+ left: auto;
+ right: 125%;
+ transform: none;
+}
+
+.tooltip-left .tooltip-text::after {
+ left: auto;
+ right: -4px;
+ transform: rotate(135deg);
+}
+
+.tooltip-right .tooltip-text {
+ left: 125%;
+ transform: none;
+}
+
+.tooltip-right .tooltip-text::after {
+ left: -4px;
+ right: auto;
+ transform: rotate(315deg);
+}
+
+/* Motion Safety - Respect prefers-reduced-motion */
+
+@media (prefers-reduced-motion: reduce) {
+ /* Remove all animations */
+
+ *,
+ *::before,
+ *::after {
+ animation: none !important;
+ transition: none !important;
+ }
+
+ /* Ensure focus-visible is still visible */
+
+ :focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ }
+}
+
+.hover\:bg-surface:hover {
+ background-color: var(--surface);
+}
+
+.hover\:text-accent:hover {
+ color: var(--accent);
+}
+
+.hover\:text-accent2:hover {
+ color: var(--accent2);
+}
+
+.hover\:text-text:hover {
+ color: var(--text);
+}
+
+.group:hover .group-hover\:text-accent {
+ color: var(--accent);
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:prose-invert {
+ --tw-prose-body: var(--tw-prose-invert-body);
+ --tw-prose-headings: var(--tw-prose-invert-headings);
+ --tw-prose-lead: var(--tw-prose-invert-lead);
+ --tw-prose-links: var(--tw-prose-invert-links);
+ --tw-prose-bold: var(--tw-prose-invert-bold);
+ --tw-prose-counters: var(--tw-prose-invert-counters);
+ --tw-prose-bullets: var(--tw-prose-invert-bullets);
+ --tw-prose-hr: var(--tw-prose-invert-hr);
+ --tw-prose-quotes: var(--tw-prose-invert-quotes);
+ --tw-prose-quote-borders: var(--tw-prose-invert-quote-borders);
+ --tw-prose-captions: var(--tw-prose-invert-captions);
+ --tw-prose-kbd: var(--tw-prose-invert-kbd);
+ --tw-prose-kbd-shadows: var(--tw-prose-invert-kbd-shadows);
+ --tw-prose-code: var(--tw-prose-invert-code);
+ --tw-prose-pre-code: var(--tw-prose-invert-pre-code);
+ --tw-prose-pre-bg: var(--tw-prose-invert-pre-bg);
+ --tw-prose-th-borders: var(--tw-prose-invert-th-borders);
+ --tw-prose-td-borders: var(--tw-prose-invert-td-borders);
+ }
+
+ .dark\:hover\:text-text:hover {
+ color: var(--text);
+ }
+}
+
+.first-letter\:text-3xl::first-letter {
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+}
+
+.hover\:bg-surface:hover {
+ background-color: var(--surface);
+}
+
+.hover\:text-accent:hover {
+ color: var(--accent);
+}
+
+.hover\:text-accent2:hover {
+ color: var(--accent2);
+}
+
+.hover\:text-text:hover {
+ color: var(--text);
+}
+
+.hover\:underline:hover {
+ text-decoration-line: underline;
+}
+
+.hover\:opacity-80:hover {
+ opacity: 0.8;
+}
+
+.hover\:opacity-90:hover {
+ opacity: 0.9;
+}
+
+.hover\:shadow-lg:hover {
+ --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
+ --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
+}
+
+.focus\:not-sr-only:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ padding: 0;
+ margin: 0;
+ overflow: visible;
+ clip: auto;
+ white-space: normal;
+}
+
+.focus\:fixed:focus {
+ position: fixed;
+}
+
+.focus\:left-4:focus {
+ left: 1rem;
+}
+
+.focus\:top-4:focus {
+ top: 1rem;
+}
+
+.focus\:z-50:focus {
+ z-index: 50;
+}
+
+.focus\:rounded:focus {
+ border-radius: 0.25rem;
+}
+
+.focus\:border-accent:focus {
+ border-color: var(--accent);
+}
+
+.focus\:border-transparent:focus {
+ border-color: transparent;
+}
+
+.focus\:bg-accent:focus {
+ background-color: var(--accent);
+}
+
+.focus\:px-4:focus {
+ padding-left: 1rem;
+ padding-right: 1rem;
+}
+
+.focus\:py-2:focus {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.focus\:text-white:focus {
+ --tw-text-opacity: 1;
+ color: rgb(255 255 255 / var(--tw-text-opacity, 1));
+}
+
+.focus\:outline-none:focus {
+ outline: 2px solid transparent;
+ outline-offset: 2px;
+}
+
+.focus\:ring-1:focus {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+
+.focus\:ring-2:focus {
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
+}
+
+.focus\:ring-accent:focus {
+ --tw-ring-color: var(--accent);
+}
+
+.disabled\:cursor-not-allowed:disabled {
+ cursor: not-allowed;
+}
+
+.disabled\:opacity-50:disabled {
+ opacity: 0.5;
+}
+
+.group:hover .group-hover\:scale-105 {
+ --tw-scale-x: 1.05;
+ --tw-scale-y: 1.05;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.group:hover .group-hover\:text-accent {
+ color: var(--accent);
+}
+
+@media (min-width: 640px) {
+ .sm\:flex-row {
+ flex-direction: row;
+ }
+}
+
+@media (min-width: 768px) {
+ .md\:order-none {
+ order: 0;
+ }
+
+ .md\:col-span-1 {
+ grid-column: span 1 / span 1;
+ }
+
+ .md\:col-span-2 {
+ grid-column: span 2 / span 2;
+ }
+
+ .md\:inline {
+ display: inline;
+ }
+
+ .md\:flex {
+ display: flex;
+ }
+
+ .md\:hidden {
+ display: none;
+ }
+
+ .md\:h-48 {
+ height: 12rem;
+ }
+
+ .md\:w-48 {
+ width: 12rem;
+ }
+
+ .md\:max-w-\[45\%\] {
+ max-width: 45%;
+ }
+
+ .md\:grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .md\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+
+ .md\:gap-6 {
+ gap: 1.5rem;
+ }
+
+ .md\:text-4xl {
+ font-size: 2.25rem;
+ line-height: 2.5rem;
+ }
+
+ .md\:text-5xl {
+ font-size: 3rem;
+ line-height: 1;
+ }
+
+ .md\:text-6xl {
+ font-size: 3.75rem;
+ line-height: 1;
+ }
+
+ .md\:text-8xl {
+ font-size: 6rem;
+ line-height: 1;
+ }
+}
+
+@media (min-width: 1060px) {
+ .lg\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .dark\:hover\:text-text:hover {
+ color: var(--text);
+ }
+}
diff --git a/assets/css/variables.css b/assets/css/variables.css
deleted file mode 100644
index 173e12c..0000000
--- a/assets/css/variables.css
+++ /dev/null
@@ -1,136 +0,0 @@
-/* variables.css */
-
-:root {
- /* Dark theme colors (default) */
- --bg: #060b10;
- --bg2: #0c1520;
- --surface: #101e2d;
- --border: #182840;
- --accent: #a855f7;
- --accent2: #00ff88;
- --accent-glow: rgba(168, 85, 247, 0.12);
- --text: #c4d6e8;
- --text-dim: #7a9bb8;
- --muted: #304860;
-
- /* Type colors (renamed from --color-*) */
- --type-tech: #a855f7; /* purple */
- --type-life: #f59e0b; /* amber */
- --type-quote: #00ff88; /* green */
- --type-link: #38bdf8; /* cyan */
- --type-photo: #ec4899; /* pink */
-
- /* Terminal palette (referenced by hero.css and 404.css) */
- --terminal-prompt: var(--accent2);
- --terminal-text: var(--text);
- --terminal-accent: var(--type-link);
-
- /* Typography */
- --font-body: 'IBM Plex Sans', system-ui, sans-serif;
- --font-mono: 'JetBrains Mono', 'Courier New', monospace;
- --font-head: 'Oxanium', sans-serif;
-
- /* Font sizes (base: 17px on html) */
- --fs-body: 0.95rem;
- --fs-nav: 0.8rem;
- --fs-badge: 0.7rem;
- --fs-btn: 0.8rem;
- --fs-h3: 1.5rem;
- --fs-h2: clamp(1.7rem, 6vw, 3rem);
-
- /* Layout */
- --container-max: 1080px;
- --container-narrow: 768px;
- --gap-sm: 0.5rem;
- --gap-md: 1.5rem;
- --gap-lg: 2.5rem;
- --gap-xl: 4rem;
-
- /* Spacing scale */
- --sp-1: 0.25rem;
- --sp-2: 0.5rem;
- --sp-3: 0.75rem;
- --sp-4: 1rem;
- --sp-5: 1.25rem;
- --sp-6: 1.5rem;
- --sp-7: 1.75rem;
- --sp-8: 2rem;
- --sp-9: 2.25rem;
- --sp-10: 2.5rem;
- --sp-12: 3rem;
- --sp-14: 3.5rem;
- --sp-16: 4rem;
-
- /* Z-index scale */
- --z-base: 1;
- --z-matrix: 0;
- --z-nav: 100;
- --z-menu: 99;
- --z-modal: 200;
- --z-progress: 9999;
-
- /* Timing */
- --duration-fast: 100ms;
- --duration-base: 300ms;
- --duration-slow: 500ms;
-
- /* Easing */
- --ease-out: cubic-bezier(0.33, 1, 0.68, 1);
- --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
-
- /* Section spacing */
- --section-py-mobile: 4rem;
- --section-py-desktop: 6rem;
- --card-px-mobile: 1.5rem;
- --card-px-desktop: 2rem;
-
- /* Transitions (legacy, keep for compatibility) */
- --transition: all 0.2s ease;
- --transition-slow: all 0.75s cubic-bezier(0.16,1,0.3,1);
-}
-
-html.theme-light {
- /* Light theme */
- --bg: #f0f4f8;
- --bg2: #e2eaf4;
- --surface: #d4dff0;
- --border: #a8bdd8;
- --accent: #7c3aed;
- --accent2: #008f5a;
- --accent-glow: rgba(124, 58, 237, 0.08);
- --text: #0d1b2a;
- --text-dim: #2e4a6a;
- --muted: #6888a8;
-
- /* Type colors (light equivalents) */
- --type-tech: #7c3aed; /* purple */
- --type-life: #d97706; /* amber */
- --type-quote: #008f5a; /* green */
- --type-link: #0284c7; /* cyan */
- --type-photo: #be185d; /* pink */
-}
-
-/* Breakpoints as CSS variables for reference */
-@media (max-width: 479px) {
- :root {
- --bp: "mobile";
- }
-}
-
-@media (min-width: 480px) {
- :root {
- --bp: "sm";
- }
-}
-
-@media (min-width: 768px) {
- :root {
- --bp: "md";
- }
-}
-
-@media (min-width: 1200px) {
- :root {
- --bp: "lg";
- }
-}
diff --git a/assets/js/404.js b/assets/js/404.js
deleted file mode 100644
index c26c218..0000000
--- a/assets/js/404.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * 404.js
- * Quote randomization and terminal animation for 404 page
- */
-
-(function() {
- 'use strict';
-
- const quotes = [
- 'The page you are looking for doesn\'t exist. But that\'s okay, nothing exists until you find it.',
- 'A 404 is just a redirect to a new beginning.',
- 'You found a secret path. Sadly, it leads nowhere.',
- 'This page chose to remain unknown.',
- 'In the quantum realm, this page exists and doesn\'t exist simultaneously.',
- 'Sometimes the best discoveries are the ones we never intended to find.',
- ];
-
- const quoteText = document.getElementById('quote-text');
- const quoteAuthor = document.getElementById('quote-author');
- const terminalFiles = document.getElementById('terminal-files');
-
- if (quoteText && quoteAuthor) {
- const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
- quoteText.textContent = randomQuote;
- quoteAuthor.textContent = '— 404 Philosopher';
- }
-
- if (terminalFiles) {
- const files = [
- 'post-01-security.md',
- 'post-02-web-dev.md',
- 'post-03-bash-tips.md',
- 'about.md',
- 'contact.md',
- ];
-
- files.forEach((file) => {
- const line = document.createElement('div');
- line.textContent = file;
- terminalFiles.appendChild(line);
- });
- }
-
- // Listen for search (if implemented)
- const searchBtn = document.querySelector('.search-box button');
- if (searchBtn) {
- searchBtn.addEventListener('click', () => {
- const input = document.querySelector('.search-box input');
- if (input && input.value) {
- window.location.href = `/articles/?search=${encodeURIComponent(input.value)}`;
- }
- });
- }
-})();
diff --git a/assets/js/article-lazy.js b/assets/js/article-lazy.js
new file mode 100644
index 0000000..64ca862
--- /dev/null
+++ b/assets/js/article-lazy.js
@@ -0,0 +1,34 @@
+document.addEventListener('DOMContentLoaded', function () {
+ var timeline = document.querySelector('ol.timeline');
+ if (!timeline) return;
+
+ // Progressive enhancement: activates CSS hidden state
+ timeline.classList.add('js-lazy-timeline');
+
+ var items = Array.prototype.slice.call(
+ timeline.querySelectorAll('.timeline-item')
+ );
+
+ function reveal(item) {
+ item.classList.add('is-visible');
+ }
+
+ var observer = new IntersectionObserver(
+ function (entries) {
+ entries.forEach(function (entry) {
+ if (entry.isIntersecting) {
+ reveal(entry.target);
+ observer.unobserve(entry.target);
+ }
+ });
+ },
+ {
+ rootMargin: '-80px 0px 0px 0px',
+ threshold: 0.12,
+ }
+ );
+
+ items.forEach(function (item) {
+ observer.observe(item);
+ });
+});
diff --git a/assets/js/code-copy.js b/assets/js/code-copy.js
new file mode 100644
index 0000000..8591436
--- /dev/null
+++ b/assets/js/code-copy.js
@@ -0,0 +1,79 @@
+(function () {
+ var LANG_NAMES = {
+ bash: 'Shell', sh: 'Shell', shell: 'Shell', zsh: 'Shell',
+ js: 'JavaScript', javascript: 'JavaScript',
+ ts: 'TypeScript', typescript: 'TypeScript',
+ go: 'Go',
+ py: 'Python', python: 'Python',
+ rs: 'Rust', rust: 'Rust',
+ html: 'HTML',
+ css: 'CSS',
+ toml: 'TOML',
+ yaml: 'YAML', yml: 'YAML',
+ json: 'JSON',
+ sql: 'SQL',
+ md: 'Markdown', markdown: 'Markdown',
+ c: 'C',
+ cpp: 'C++', 'c++': 'C++',
+ java: 'Java',
+ php: 'PHP',
+ ruby: 'Ruby', rb: 'Ruby',
+ swift: 'Swift',
+ kotlin: 'Kotlin', kt: 'Kotlin',
+ dockerfile: 'Dockerfile',
+ makefile: 'Makefile',
+ text: 'Text', txt: 'Text',
+ };
+
+ function prettyName(lang) {
+ if (!lang) return '';
+ var key = lang.toLowerCase();
+ return LANG_NAMES[key] || (lang.charAt(0).toUpperCase() + lang.slice(1));
+ }
+
+ function getCodeText(wrapper) {
+ var el = wrapper.querySelector('.lntd:last-child code')
+ || wrapper.querySelector('.code-body code')
+ || wrapper.querySelector('.code-body pre');
+ return el ? el.innerText : '';
+ }
+
+ function initBlock(wrapper) {
+ var header = wrapper.querySelector('.code-header');
+ if (header) {
+ var label = wrapper.querySelector('.code-lang-label');
+ if (label) label.textContent = prettyName(header.getAttribute('data-lang') || '');
+ }
+
+ var btn = wrapper.querySelector('[data-copy-target]');
+ if (!btn) return;
+
+ btn.addEventListener('click', function () {
+ var text = getCodeText(wrapper);
+ if (!text) return;
+
+ navigator.clipboard.writeText(text).then(function () {
+ var copyIcon = btn.querySelector('.icon-copy');
+ var checkIcon = btn.querySelector('.icon-check');
+ var liveRegion = wrapper.querySelector('.code-copy-status');
+ if (copyIcon) copyIcon.classList.add('hidden');
+ if (checkIcon) checkIcon.classList.remove('hidden');
+ btn.classList.add('is-copied');
+ if (liveRegion) liveRegion.textContent = 'Code copied to clipboard.';
+
+ setTimeout(function () {
+ if (copyIcon) copyIcon.classList.remove('hidden');
+ if (checkIcon) checkIcon.classList.add('hidden');
+ btn.classList.remove('is-copied');
+ if (liveRegion) liveRegion.textContent = '';
+ }, 2000);
+ }).catch(function () {
+ // silent fail for insecure contexts
+ });
+ });
+ }
+
+ document.addEventListener('DOMContentLoaded', function () {
+ document.querySelectorAll('.code-block-wrapper').forEach(initBlock);
+ });
+})();
diff --git a/assets/js/contact-form.js b/assets/js/contact-form.js
new file mode 100644
index 0000000..4fa8f55
--- /dev/null
+++ b/assets/js/contact-form.js
@@ -0,0 +1,45 @@
+document.addEventListener('alpine:init', () => {
+ Alpine.data('contactForm', () => ({
+ formData: {
+ name: '',
+ email: '',
+ message: ''
+ },
+ isSubmitting: false,
+ statusMessage: '',
+ statusClass: '',
+
+ async submitContactForm() {
+ this.isSubmitting = true;
+ this.statusMessage = '';
+ this.statusClass = '';
+
+ try {
+ const response = await fetch('/contact.php', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(this.formData)
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ this.statusMessage = 'Message sent successfully!';
+ this.statusClass = 'bg-green-100 text-green-800 border border-green-300';
+ this.formData = { name: '', email: '', message: '' };
+ } else {
+ this.statusMessage = data.error || 'An error occurred. Please try again.';
+ this.statusClass = 'bg-red-100 text-red-800 border border-red-300';
+ }
+ } catch (error) {
+ this.statusMessage = 'An error occurred. Please try again.';
+ this.statusClass = 'bg-red-100 text-red-800 border border-red-300';
+ console.error('Form submission error:', error);
+ } finally {
+ this.isSubmitting = false;
+ }
+ }
+ }));
+});
diff --git a/assets/js/copy-code.js b/assets/js/copy-code.js
deleted file mode 100644
index a18bf6c..0000000
--- a/assets/js/copy-code.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// copy-code.js
-(function() {
- // Add copy button to all code blocks
- const codeBlocks = document.querySelectorAll('pre, .highlight');
-
- codeBlocks.forEach(block => {
- // Create copy button
- const btn = document.createElement('button');
- btn.className = 'code-copy-btn';
- btn.textContent = 'copy';
- btn.type = 'button';
- btn.setAttribute('aria-label', 'Copy code');
-
- // Get code text
- const code = block.querySelector('code');
- const text = code ? code.textContent : block.textContent;
-
- // Copy on click
- btn.addEventListener('click', async function() {
- try {
- await navigator.clipboard.writeText(text);
-
- // Show feedback
- const originalText = btn.textContent;
- btn.textContent = 'copied!';
- btn.classList.add('copied');
-
- setTimeout(() => {
- btn.textContent = originalText;
- btn.classList.remove('copied');
- }, 2000);
- } catch (err) {
- console.error('Failed to copy:', err);
- btn.textContent = 'error';
- }
- });
-
- // Add button to block
- block.style.position = 'relative';
- block.appendChild(btn);
- });
-})();
diff --git a/assets/js/filters.js b/assets/js/filters.js
deleted file mode 100644
index f7fa6a6..0000000
--- a/assets/js/filters.js
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * filters.js
- * Article filtering by type on the articles page
- */
-
-(function() {
- 'use strict';
-
- const filterBtns = document.querySelectorAll('.filter-btn');
- const timelineItems = document.querySelectorAll('.timeline-item');
-
- filterBtns.forEach((btn) => {
- btn.addEventListener('click', () => {
- const filter = btn.getAttribute('data-filter');
-
- // Update active button
- filterBtns.forEach((b) => b.classList.remove('active'));
- btn.classList.add('active');
-
- // Filter articles
- timelineItems.forEach((item) => {
- const type = item.getAttribute('data-type');
-
- if (filter === 'all' || type === filter) {
- item.classList.add('visible');
- } else {
- item.classList.remove('visible');
- }
- });
- });
- });
-
- // Show all on load
- timelineItems.forEach((item) => {
- item.classList.add('visible');
- });
-})();
diff --git a/assets/js/form-components.js b/assets/js/form-components.js
new file mode 100644
index 0000000..ffa4260
--- /dev/null
+++ b/assets/js/form-components.js
@@ -0,0 +1,127 @@
+// Form component utilities and Alpine.js data
+
+export function formComponentsData() {
+ return {
+ // Modal states
+ showAlertModal: false,
+ showConfirmModal: false,
+ showContentModal: false,
+
+ // Toast notification state
+ toasts: [],
+
+ // Handle confirm modal action
+ handleConfirm() {
+ this.showConfirmModal = false;
+ this.showToast('success', 'Action confirmed!');
+ },
+
+ // Show toast notification
+ showToast(type = 'success', message = null) {
+ const messages = {
+ success: 'Operation completed successfully!',
+ error: 'An error occurred. Please try again.',
+ info: 'Here is some information.',
+ warning: 'Please be careful with this action.'
+ };
+
+ const toastMessage = message || messages[type] || messages.success;
+ const toastId = Date.now();
+
+ // Add toast to list
+ this.toasts.push({
+ id: toastId,
+ type: type,
+ message: toastMessage
+ });
+
+ // Auto-remove after 5 seconds
+ setTimeout(() => {
+ this.toasts = this.toasts.filter(t => t.id !== toastId);
+ }, 5000);
+ },
+
+ // Remove toast manually
+ removeToast(id) {
+ this.toasts = this.toasts.filter(t => t.id !== id);
+ },
+
+ // Form validation utilities
+ validateEmail(email) {
+ const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ return regex.test(email);
+ },
+
+ validatePassword(password) {
+ return password.length >= 8;
+ },
+
+ // Auto-expand textarea
+ autoExpandTextarea(event) {
+ const textarea = event.target;
+ textarea.style.height = 'auto';
+ textarea.style.height = (textarea.scrollHeight) + 'px';
+ }
+ };
+}
+
+// Toast container component for Alpine.js
+export function renderToastContainer(Alpine) {
+ if (!Alpine) return;
+
+ // This can be used in templates via Alpine
+ window.formUtils = {
+ formatCharCount(current, max) {
+ if (max) {
+ return `${current}/${max}`;
+ }
+ return current;
+ },
+
+ isCharCountWarning(current, max) {
+ if (!max) return false;
+ return current > (max * 0.8);
+ },
+
+ isCharCountError(current, max) {
+ if (!max) return false;
+ return current >= max;
+ }
+ };
+}
+
+// Focus Trap for Modals - Week 5
+function createFocusTrap(modalElement) {
+ const focusableElements = modalElement.querySelectorAll(
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
+ );
+
+ if (focusableElements.length === 0) return;
+
+ const firstElement = focusableElements[0];
+ const lastElement = focusableElements[focusableElements.length - 1];
+
+ modalElement.addEventListener('keydown', (e) => {
+ if (e.key !== 'Tab') return;
+
+ if (e.shiftKey) {
+ // Shift + Tab
+ if (document.activeElement === firstElement) {
+ e.preventDefault();
+ lastElement.focus();
+ }
+ } else {
+ // Tab
+ if (document.activeElement === lastElement) {
+ e.preventDefault();
+ firstElement.focus();
+ }
+ }
+ });
+
+ // Set initial focus
+ firstElement.focus();
+}
+
+// Export for use in Alpine.js
+window.createFocusTrap = createFocusTrap;
diff --git a/assets/js/fortune.js b/assets/js/fortune.js
new file mode 100644
index 0000000..d4f981b
--- /dev/null
+++ b/assets/js/fortune.js
@@ -0,0 +1,9 @@
+(function() {
+ const el = document.getElementById('fortune-quote');
+ if (!el) return;
+ const quotes = JSON.parse(el.dataset.quotes);
+ if (!quotes || quotes.length === 0) return;
+ const q = quotes[Math.floor(Math.random() * quotes.length)];
+ el.querySelector('.fortune-text').textContent = '"' + q.text + '"';
+ el.querySelector('.fortune-author').textContent = '— ' + q.author;
+})();
diff --git a/assets/js/glitch.js b/assets/js/glitch.js
deleted file mode 100644
index 85f8a00..0000000
--- a/assets/js/glitch.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * glitch.js
- * Random glitch effect on .hero-name every 4-11 seconds
- */
-
-export function initGlitch() {
- 'use strict';
-
- const heroName = document.querySelector('.hero-name');
- if (!heroName) return;
-
- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
-
- function triggerGlitch() {
- heroName.classList.add('is-glitching');
- setTimeout(() => {
- heroName.classList.remove('is-glitching');
- }, 450);
- }
-
- function scheduleNextGlitch() {
- const delay = Math.random() * 7000 + 4000; // 4-11 seconds
- setTimeout(() => {
- triggerGlitch();
- scheduleNextGlitch();
- }, delay);
- }
-
- scheduleNextGlitch();
-}
diff --git a/assets/js/hamburger.js b/assets/js/hamburger.js
deleted file mode 100644
index 1d27633..0000000
--- a/assets/js/hamburger.js
+++ /dev/null
@@ -1,83 +0,0 @@
-/**
- * hamburger.js
- * Hamburger menu toggle with focus trap and keyboard navigation
- */
-
-export function initHamburger() {
- 'use strict';
-
- const hamburgerBtn = document.getElementById('hamburger-btn');
- const menuOverlay = document.getElementById('menu-overlay');
- const menuLinks = document.querySelectorAll('.menu-links a');
- const themeSwitch = document.getElementById('theme-switch');
-
- if (!hamburgerBtn || !menuOverlay) return;
-
- // Toggle menu on hamburger click
- hamburgerBtn.addEventListener('click', () => {
- const isOpen = hamburgerBtn.getAttribute('aria-expanded') === 'true';
- setMenuOpen(!isOpen);
- });
-
- // Close menu on overlay click
- menuOverlay.addEventListener('click', (e) => {
- if (e.target === menuOverlay) {
- setMenuOpen(false);
- }
- });
-
- // Close menu on menu link click
- menuLinks.forEach((link) => {
- link.addEventListener('click', () => {
- setMenuOpen(false);
- });
- });
-
- // Close menu on Escape key
- document.addEventListener('keydown', (e) => {
- if (e.key === 'Escape') {
- setMenuOpen(false);
- }
- });
-
- // Focus trap: Tab through menu items and theme switch
- menuOverlay.addEventListener('keydown', (e) => {
- if (e.key !== 'Tab') return;
-
- const focusableElements = Array.from(
- menuOverlay.querySelectorAll('a, button')
- );
- const firstElement = focusableElements[0];
- const lastElement = focusableElements[focusableElements.length - 1];
-
- if (e.shiftKey) {
- if (document.activeElement === firstElement) {
- lastElement.focus();
- e.preventDefault();
- }
- } else {
- if (document.activeElement === lastElement) {
- firstElement.focus();
- e.preventDefault();
- }
- }
- });
-
- function setMenuOpen(isOpen) {
- hamburgerBtn.setAttribute('aria-expanded', isOpen);
- hamburgerBtn.classList.toggle('active', isOpen);
- menuOverlay.classList.toggle('active', isOpen);
-
- if (isOpen) {
- document.body.style.overflow = 'hidden';
- // Focus the first menu link
- const firstLink = menuLinks[0];
- if (firstLink) {
- setTimeout(() => firstLink.focus(), 100);
- }
- } else {
- document.body.style.overflow = '';
- hamburgerBtn.focus();
- }
- }
-}
diff --git a/assets/js/lightbox.js b/assets/js/lightbox.js
deleted file mode 100644
index 81c3613..0000000
--- a/assets/js/lightbox.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/**
- * lightbox.js
- * Photo lightbox initialization
- */
-
-(function() {
- 'use strict';
-
- if (typeof PhotoUtils === 'undefined') return;
-
- const photoGrid = document.querySelector('.photo-grid[data-lightbox="true"]');
- if (!photoGrid) return;
-
- const photoCards = photoGrid.querySelectorAll('.photo-card');
- const photosData = [];
-
- photoCards.forEach((card, index) => {
- const figure = card.querySelector('figure');
- const img = card.querySelector('img');
- photosData.push({
- index,
- src: figure.getAttribute('data-src') || img.src,
- alt: figure.getAttribute('data-alt') || img.alt,
- caption: figure.getAttribute('data-caption'),
- location: figure.getAttribute('data-location'),
- });
- });
-
- // Initialize lightbox with PhotoUtils
- PhotoUtils.initLightbox('.photo-grid', photosData);
-})();
diff --git a/assets/js/main.js b/assets/js/main.js
deleted file mode 100644
index ee6b24f..0000000
--- a/assets/js/main.js
+++ /dev/null
@@ -1,18 +0,0 @@
-// main.js
-import './theme-toggle.js';
-import './matrix-rain.js';
-import './progress-bar.js';
-import './copy-code.js';
-import { initHamburger } from './hamburger.js';
-import { initGlitch } from './glitch.js';
-import { initTyping } from './typing.js';
-import { initScrollReveal } from './scroll-reveal.js';
-import { initShareSidebar } from './share-sidebar.js';
-
-document.addEventListener('DOMContentLoaded', () => {
- initHamburger();
- initGlitch();
- initTyping();
- initScrollReveal();
- initShareSidebar();
-});
diff --git a/assets/js/matrix-rain.js b/assets/js/matrix-rain.js
index 8211b40..53c55d8 100644
--- a/assets/js/matrix-rain.js
+++ b/assets/js/matrix-rain.js
@@ -1,56 +1,157 @@
-// matrix-rain.js
-(function () {
- const canvas = document.getElementById('matrix-canvas');
+// Matrix rain background effect
+(function() {
+ // Bail out if user prefers reduced motion
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ return;
+ }
+
+ // Canvas and context
+ let canvas = document.getElementById('matrix-rain');
if (!canvas) return;
- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+ const ctx = canvas.getContext('2d');
+
+ // State
+ let columns = [];
+ let frameCount = 0;
+ let colors = { accent: '#a855f7', accent2: '#00ff88', bg: '#060b10', head: '#ffffff' };
+
+ // Character set: 30% ASCII, 70% katakana
+ const ASCII = Array.from({ length: 94 }, (_, i) => String.fromCharCode(33 + i));
+ const KATA = Array.from({ length: 96 }, (_, i) => String.fromCodePoint(0x30a0 + i));
+ const CHARS = shuffle([...ASCII, ...KATA, ...KATA, ...KATA]);
+
+ // Utility: shuffle array
+ function shuffle(arr) {
+ for (let i = arr.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [arr[i], arr[j]] = [arr[j], arr[i]];
+ }
+ return arr;
+ }
+
+ // Utility: convert hex or rgb color to rgba string
+ function hexToRgba(color, alpha) {
+ const rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
+ if (rgbMatch) {
+ return `rgba(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]}, ${alpha})`;
+ }
+ const hex = color.replace('#', '');
+ const r = parseInt(hex.substring(0, 2), 16);
+ const g = parseInt(hex.substring(2, 4), 16);
+ const b = parseInt(hex.substring(4, 6), 16);
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+ }
+
+ // Sample CSS variables based on current theme
+ function sampleColors() {
+ const style = getComputedStyle(document.documentElement);
+ const isDark = document.documentElement.classList.contains('theme-dark');
+ colors.accent = style.getPropertyValue('--accent').trim();
+ colors.accent2 = style.getPropertyValue('--accent2').trim();
+ colors.bg = style.getPropertyValue('--bg').trim();
+ // Head char: bright white in dark mode, deep purple-black in light mode
+ colors.head = isDark ? '#ffffff' : '#1a0533';
+ }
- const ctx = canvas.getContext('2d');
- const CHARS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789ABCDEF<>/\\|{}[]$#@!';
- const FS = 14; // font size / column width in px
- const mode = canvas.getAttribute('data-mode') || 'background';
- let cols, drops, raf;
+ // Resize canvas to window dimensions
+ function resizeCanvas() {
+ canvas.width = window.innerWidth;
+ canvas.height = window.innerHeight;
+ ctx.font = '14px "JetBrains Mono", monospace';
+ ctx.textBaseline = 'top';
+ initColumns();
+ }
+
+ // Initialize columns for the current canvas width
+ function initColumns() {
+ columns = [];
+ const columnWidth = 14;
+ const columnCount = Math.floor(canvas.width / columnWidth);
- function init() {
- // Use offsetWidth/offsetHeight which works for both fixed and positioned elements
- canvas.width = canvas.offsetWidth || window.innerWidth;
- canvas.height = canvas.offsetHeight || window.innerHeight;
- cols = Math.floor(canvas.width / FS) + 1;
- drops = Array.from({ length: cols }, () => Math.random() * -(canvas.height / FS));
+ for (let i = 0; i < columnCount; i++) {
+ columns.push({
+ x: i * columnWidth,
+ y: -Math.floor(Math.random() * 40), // stagger start above viewport
+ speed: 2 + Math.floor(Math.random() * 3), // 2-4 frames between drops
+ color: Math.random() < 0.6 ? 'accent2' : 'accent', // 60% green, 40% purple
+ charIndex: Math.floor(Math.random() * CHARS.length),
+ length: 8 + Math.floor(Math.random() * 13), // trail length 8-20
+ });
+ }
}
- function getThemeColors() {
- const isDark = !document.documentElement.classList.contains('theme-light');
- return {
- bgFill: isDark ? 'rgba(6, 11, 16, 0.07)' : 'rgba(240, 244, 248, 0.07)',
- };
+ // Set up MutationObserver for theme switching
+ function setupThemeObserver() {
+ const observer = new MutationObserver(function(mutations) {
+ for (const m of mutations) {
+ if (m.attributeName === 'class') {
+ sampleColors();
+ break;
+ }
+ }
+ });
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['class'],
+ });
}
- function tick() {
- const colors = getThemeColors();
- ctx.fillStyle = colors.bgFill;
+ // Main animation loop
+ function drawFrame() {
+ frameCount++;
+
+ // Fade layer: semi-transparent background fill
+ ctx.fillStyle = hexToRgba(colors.bg, 0.085);
ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.font = `${FS}px "JetBrains Mono", monospace`;
- for (let i = 0; i < cols; i++) {
- const char = CHARS[Math.floor(Math.random() * CHARS.length)];
- // Occasional bright green "head", otherwise purple
- ctx.fillStyle = Math.random() > 0.96 ? '#00ff88' : '#a855f7';
- ctx.fillText(char, i * FS, drops[i] * FS);
+ // Draw each column
+ for (const col of columns) {
+ // Skip if not time to drop yet (per-column throttle)
+ if (frameCount % col.speed !== 0) continue;
+
+ // Draw explicit trail in column color
+ ctx.fillStyle = colors[col.color];
+ for (let i = 1; i <= col.length; i++) {
+ const trailY = (col.y - i) * 14;
+ if (trailY < 0) continue;
+ const trailCharIndex = (col.charIndex - i + CHARS.length) % CHARS.length;
+ ctx.fillText(CHARS[trailCharIndex], col.x, trailY);
+ }
+
+ // Draw head character (bright)
+ ctx.fillStyle = colors.head;
+ const headCharIndex = col.charIndex % CHARS.length;
+ ctx.fillText(CHARS[headCharIndex], col.x, col.y * 14);
+
+ // Advance column
+ col.y++;
+ col.charIndex = (col.charIndex + 1) % CHARS.length;
- if (drops[i] * FS > canvas.height && Math.random() > 0.975) {
- drops[i] = Math.random() * -20;
+ // Reset when scrolled off screen
+ if (col.y * 14 > canvas.height + col.length * 14) {
+ col.y = -Math.floor(Math.random() * 20);
+ col.charIndex = Math.floor(Math.random() * CHARS.length);
+ col.color = Math.random() < 0.6 ? 'accent2' : 'accent';
}
- drops[i] += 0.5;
}
- raf = requestAnimationFrame(tick);
+
+ requestAnimationFrame(drawFrame);
}
- init();
- window.addEventListener('resize', () => { cancelAnimationFrame(raf); init(); tick(); }, { passive: true });
- document.addEventListener('visibilitychange', () => {
- if (document.hidden) cancelAnimationFrame(raf); else tick();
+ // Initialize
+ sampleColors();
+ resizeCanvas();
+ setupThemeObserver();
+
+ // Debounced resize handler
+ let resizeTimer;
+ window.addEventListener('resize', function() {
+ clearTimeout(resizeTimer);
+ resizeTimer = setTimeout(resizeCanvas, 150);
});
- tick();
- window.MatrixRain = { init, tick };
+ // Start animation when fonts are ready
+ document.fonts.ready.then(function() {
+ requestAnimationFrame(drawFrame);
+ });
})();
diff --git a/assets/js/menu.js b/assets/js/menu.js
new file mode 100644
index 0000000..3f32642
--- /dev/null
+++ b/assets/js/menu.js
@@ -0,0 +1,112 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const menuToggle = document.getElementById('menu-toggle');
+ const menuOverlay = document.getElementById('menu-overlay');
+ const menuPanel = document.getElementById('hamburger-menu');
+
+ function openMenu() {
+ if (!menuOverlay || !menuPanel) return;
+
+ // Show overlay
+ menuOverlay.classList.remove('opacity-0');
+ menuOverlay.classList.remove('invisible');
+
+ // Slide menu in
+ menuPanel.classList.remove('translate-x-full');
+
+ // Manage accessibility
+ menuToggle.setAttribute('aria-expanded', 'true');
+ menuPanel.removeAttribute('aria-hidden');
+
+ // Control body overflow
+ document.body.style.overflow = 'hidden';
+
+ // Focus first focusable element in menu
+ const firstFocusable = menuPanel.querySelector('a, button');
+ if (firstFocusable) {
+ setTimeout(() => firstFocusable.focus(), 50);
+ }
+ }
+
+ function closeMenu() {
+ if (!menuOverlay || menuOverlay.classList.contains('opacity-0')) return;
+
+ // Hide overlay
+ menuOverlay.classList.add('opacity-0');
+ menuOverlay.classList.add('invisible');
+
+ // Slide menu out
+ menuPanel.classList.add('translate-x-full');
+
+ // Manage accessibility
+ menuToggle.setAttribute('aria-expanded', 'false');
+ menuPanel.setAttribute('aria-hidden', 'true');
+
+ // Restore body overflow
+ document.body.style.overflow = '';
+
+ // Return focus to toggle button
+ menuToggle.focus();
+ }
+
+ function toggleMenu() {
+ if (menuOverlay && menuOverlay.classList.contains('opacity-0')) {
+ openMenu();
+ } else {
+ closeMenu();
+ }
+ }
+
+ // Toggle menu when clicking the hamburger button
+ if (menuToggle) {
+ menuToggle.addEventListener('click', toggleMenu);
+ }
+
+ // Close menu when clicking on the overlay
+ if (menuOverlay) {
+ menuOverlay.addEventListener('click', (e) => {
+ if (e.target === menuOverlay) {
+ closeMenu();
+ }
+ });
+ }
+
+ // Close menu when clicking menu items
+ const menuLinks = document.querySelectorAll('#hamburger-menu a, #hamburger-menu button');
+ menuLinks.forEach(link => {
+ link.addEventListener('click', closeMenu);
+ });
+
+ // Close menu on Escape key
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && menuOverlay && !menuOverlay.classList.contains('opacity-0')) {
+ closeMenu();
+ }
+ });
+
+ // Focus trap: keep tab within menu when open
+ if (menuPanel) {
+ menuPanel.addEventListener('keydown', (e) => {
+ if (e.key !== 'Tab') return;
+
+ const focusableElements = menuPanel.querySelectorAll('a, button, [tabindex]:not([tabindex="-1"])');
+ if (focusableElements.length === 0) return;
+
+ const firstElement = focusableElements[0];
+ const lastElement = focusableElements[focusableElements.length - 1];
+ const isMenuOpen = !menuOverlay.classList.contains('opacity-0');
+
+ if (!isMenuOpen) return;
+
+ // Shift+Tab on first element: move to last
+ if (e.shiftKey && document.activeElement === firstElement) {
+ e.preventDefault();
+ lastElement.focus();
+ }
+ // Tab on last element: move to first
+ else if (!e.shiftKey && document.activeElement === lastElement) {
+ e.preventDefault();
+ firstElement.focus();
+ }
+ });
+ }
+});
diff --git a/assets/js/not-found-page.js b/assets/js/not-found-page.js
new file mode 100644
index 0000000..2b4f676
--- /dev/null
+++ b/assets/js/not-found-page.js
@@ -0,0 +1,9 @@
+// 404 page: initialize shared notFoundPage Alpine component
+document.addEventListener('alpine:init', () => {
+ // Ensure search index is preloaded on 404 page
+ const notFoundElement = document.querySelector('[x-data*="notFoundPage"]');
+ if (notFoundElement && notFoundElement.__x) {
+ notFoundElement.__x.$data.init();
+ }
+ console.log('404 page initialized with shared search functionality');
+});
diff --git a/assets/js/photo-utils.js b/assets/js/photo-utils.js
deleted file mode 100644
index 32ede76..0000000
--- a/assets/js/photo-utils.js
+++ /dev/null
@@ -1,476 +0,0 @@
-(function() {
- 'use strict';
-
- // Internal state
- let currentPhotoIndex = 0;
- let photosData = [];
- let isOpen = false;
- let exifData = {};
- let options = {
- swipeEnabled: true,
- keyboardEnabled: true,
- extractEXIF: true,
- onOpen: null,
- onClose: null,
- };
-
- // Lightbox DOM elements (created on first init)
- let lightboxModal = null;
- let lightboxImage = null;
- let lightboxCaption = null;
- let lightboxSidebar = null;
-
- /**
- * Initialize lightbox for a photo collection
- * @param {string} containerSelector - CSS selector for photo container
- * @param {Array} photos - Array of photo objects: {src, alt, caption, location}
- * @param {Object} opts - Options: {swipeEnabled, keyboardEnabled, extractEXIF, onOpen, onClose}
- * @returns {Object} - {openLightbox, closeLightbox}
- */
- function initLightbox(containerSelector, photos, opts = {}) {
- photosData = photos;
- options = { ...options, ...opts };
-
- // Merge options with defaults
- const container = document.querySelector(containerSelector);
- if (!container) {
- console.error('Photo container not found:', containerSelector);
- return { openLightbox: () => {}, closeLightbox: () => {} };
- }
-
- // Attach click listeners to all figures in the container
- const figures = container.querySelectorAll('figure[data-photo-index]');
- figures.forEach((figure) => {
- figure.addEventListener('click', () => {
- const index = parseInt(figure.dataset.photoIndex, 10);
- openLightbox(index);
- });
- });
-
- // Return public API
- return {
- openLightbox,
- closeLightbox,
- };
- }
-
- /**
- * Open lightbox to a specific photo
- * @param {number} photoIndex - 0-based index into photosData array
- */
- function openLightbox(photoIndex) {
- if (photoIndex < 0 || photoIndex >= photosData.length) {
- console.warn('Invalid photo index:', photoIndex);
- return;
- }
-
- currentPhotoIndex = photoIndex;
- isOpen = true;
-
- // Create lightbox DOM if it doesn't exist
- if (!lightboxModal) {
- createLightboxDOM();
- }
-
- // Populate lightbox with current photo
- updateLightboxContent();
-
- // Show modal
- lightboxModal.classList.add('visible');
- document.body.style.overflow = 'hidden';
-
- // Extract EXIF if enabled
- if (options.extractEXIF) {
- extractEXIFForCurrentPhoto();
- }
-
- // Attach event listeners
- attachEventListeners();
-
- // Callback
- if (options.onOpen) {
- options.onOpen(photoIndex);
- }
- }
-
- /**
- * Close lightbox
- */
- function closeLightbox() {
- if (!isOpen || !lightboxModal) return;
-
- isOpen = false;
- lightboxModal.classList.remove('visible');
- document.body.style.overflow = '';
-
- // Detach event listeners
- detachEventListeners();
-
- // Callback
- if (options.onClose) {
- options.onClose();
- }
- }
-
- /**
- * Create lightbox HTML structure and inject into DOM
- */
- function createLightboxDOM() {
- lightboxModal = document.createElement('div');
- lightboxModal.className = 'photo-lightbox';
-
- lightboxModal.innerHTML = `
- <div class="photo-lightbox-backdrop"></div>
- <div class="photo-lightbox-container">
- <button class="photo-lightbox-close" aria-label="Close lightbox">×</button>
-
- <button class="photo-lightbox-prev" aria-label="Previous photo">←</button>
-
- <div class="photo-lightbox-content">
- <img class="photo-lightbox-image" src="" alt="">
- <div class="photo-lightbox-caption"></div>
- </div>
-
- <button class="photo-lightbox-next" aria-label="Next photo">→</button>
-
- <div class="photo-lightbox-sidebar">
- <div class="photo-lightbox-sidebar-content"></div>
- </div>
- </div>
- `;
-
- document.body.appendChild(lightboxModal);
-
- // Cache element references
- lightboxImage = lightboxModal.querySelector('.photo-lightbox-image');
- lightboxCaption = lightboxModal.querySelector('.photo-lightbox-caption');
- lightboxSidebar = lightboxModal.querySelector('.photo-lightbox-sidebar-content');
-
- // Attach close button and backdrop click
- lightboxModal.querySelector('.photo-lightbox-close').addEventListener('click', closeLightbox);
- lightboxModal.querySelector('.photo-lightbox-backdrop').addEventListener('click', closeLightbox);
-
- // Attach navigation buttons
- lightboxModal.querySelector('.photo-lightbox-prev').addEventListener('click', () => navigate(-1));
- lightboxModal.querySelector('.photo-lightbox-next').addEventListener('click', () => navigate(1));
- }
-
- /**
- * Update lightbox content with current photo
- */
- function updateLightboxContent() {
- const photo = photosData[currentPhotoIndex];
- if (!photo) return;
-
- lightboxImage.src = photo.src;
- lightboxImage.alt = photo.alt || '';
-
- // Caption
- if (photo.caption) {
- lightboxCaption.textContent = photo.caption;
- lightboxCaption.style.display = 'block';
- } else {
- lightboxCaption.style.display = 'none';
- }
-
- // Update prev/next button states
- const prevBtn = lightboxModal.querySelector('.photo-lightbox-prev');
- const nextBtn = lightboxModal.querySelector('.photo-lightbox-next');
- prevBtn.disabled = currentPhotoIndex === 0;
- nextBtn.disabled = currentPhotoIndex === photosData.length - 1;
- }
-
- /**
- * Navigate to previous or next photo
- * @param {number} direction - -1 for prev, 1 for next
- */
- function navigate(direction) {
- const newIndex = currentPhotoIndex + direction;
- if (newIndex >= 0 && newIndex < photosData.length) {
- currentPhotoIndex = newIndex;
- updateLightboxContent();
-
- // Reset EXIF data and re-extract if enabled
- exifData = {};
- if (options.extractEXIF) {
- extractEXIFForCurrentPhoto();
- }
- }
- }
-
- /**
- * Extract EXIF data from current photo image element
- */
- function extractEXIFForCurrentPhoto() {
- extractEXIF(lightboxImage).then((data) => {
- exifData = data;
- updateSidebarContent();
- });
- }
-
- /**
- * Update sidebar with EXIF and location data
- */
- function updateSidebarContent() {
- const photo = photosData[currentPhotoIndex];
- let sidebarHTML = '';
-
- // Always show location if provided
- if (photo.location) {
- sidebarHTML += `
- <div class="photo-metadata-field">
- <div class="photo-metadata-label">Location</div>
- <div class="photo-metadata-value">${photo.location}</div>
- </div>
- `;
- }
-
- // Show EXIF fields if available
- if (Object.keys(exifData).length > 0) {
- sidebarHTML += `
- <div class="photo-metadata-divider"></div>
- <div class="photo-metadata-label">Camera</div>
- `;
-
- if (exifData.camera) {
- sidebarHTML += `
- <div class="photo-metadata-value">${exifData.camera}</div>
- `;
- }
- if (exifData.lens) {
- sidebarHTML += `
- <div class="photo-metadata-field">
- <div class="photo-metadata-label">Lens</div>
- <div class="photo-metadata-value">${exifData.lens}</div>
- </div>
- `;
- }
- if (exifData.focalLength) {
- sidebarHTML += `
- <div class="photo-metadata-field">
- <div class="photo-metadata-label">Focal Length</div>
- <div class="photo-metadata-value">${exifData.focalLength}</div>
- </div>
- `;
- }
- if (exifData.aperture) {
- sidebarHTML += `
- <div class="photo-metadata-field">
- <div class="photo-metadata-label">Aperture</div>
- <div class="photo-metadata-value">${exifData.aperture}</div>
- </div>
- `;
- }
- if (exifData.shutterSpeed) {
- sidebarHTML += `
- <div class="photo-metadata-field">
- <div class="photo-metadata-label">Shutter Speed</div>
- <div class="photo-metadata-value">${exifData.shutterSpeed}</div>
- </div>
- `;
- }
- if (exifData.iso) {
- sidebarHTML += `
- <div class="photo-metadata-field">
- <div class="photo-metadata-label">ISO</div>
- <div class="photo-metadata-value">${exifData.iso}</div>
- </div>
- `;
- }
- if (exifData.dateTaken) {
- sidebarHTML += `
- <div class="photo-metadata-field">
- <div class="photo-metadata-label">Date Taken</div>
- <div class="photo-metadata-value">${exifData.dateTaken}</div>
- </div>
- `;
- }
- }
-
- // Show or hide sidebar based on whether there's content
- const sidebarContainer = lightboxModal.querySelector('.photo-lightbox-sidebar');
- if (sidebarHTML) {
- lightboxSidebar.innerHTML = sidebarHTML;
- sidebarContainer.classList.add('visible');
- } else {
- sidebarContainer.classList.remove('visible');
- }
- }
-
- /**
- * Extract EXIF data from an image element using exif-js library
- * @param {HTMLImageElement} imageElement - Image to read EXIF from
- * @returns {Promise} - Resolves to {camera, lens, iso, aperture, shutterSpeed, focalLength, dateTaken}
- */
- function extractEXIF(imageElement) {
- return new Promise((resolve) => {
- // If exif-js is not loaded, return empty object
- if (typeof EXIF === 'undefined') {
- console.warn('exif-js library not loaded');
- resolve({});
- return;
- }
-
- EXIF.getData(imageElement, function() {
- const data = {
- camera: EXIF.getTag(this, 'Model'),
- lens: EXIF.getTag(this, 'LensModel'),
- iso: EXIF.getTag(this, 'ISOSpeedRatings'),
- aperture: formatAperture(EXIF.getTag(this, 'FNumber')),
- shutterSpeed: formatShutterSpeed(EXIF.getTag(this, 'ExposureTime')),
- focalLength: formatFocalLength(EXIF.getTag(this, 'FocalLength')),
- dateTaken: EXIF.getTag(this, 'DateTime'),
- };
-
- // Filter out undefined values
- Object.keys(data).forEach(key => {
- if (data[key] === undefined) {
- delete data[key];
- }
- });
-
- resolve(data);
- });
- });
- }
-
- /**
- * Format aperture value (f-number)
- */
- function formatAperture(value) {
- if (!value) return null;
- if (typeof value === 'object' && value.numerator !== undefined) {
- return `f/${(value.numerator / value.denominator).toFixed(1)}`;
- }
- return `f/${value}`;
- }
-
- /**
- * Format shutter speed (exposure time)
- */
- function formatShutterSpeed(value) {
- if (!value) return null;
- if (typeof value === 'object' && value.numerator !== undefined) {
- const speed = value.numerator / value.denominator;
- if (speed >= 1) {
- return `${speed.toFixed(1)}s`;
- }
- return `1/${Math.round(1 / speed)}`;
- }
- return value;
- }
-
- /**
- * Format focal length
- */
- function formatFocalLength(value) {
- if (!value) return null;
- if (typeof value === 'object' && value.numerator !== undefined) {
- return `${(value.numerator / value.denominator).toFixed(0)}mm`;
- }
- return `${value}mm`;
- }
-
- /**
- * Attach event listeners (swipe, keyboard, etc.)
- */
- function attachEventListeners() {
- if (options.swipeEnabled) {
- attachSwipeListeners();
- }
- if (options.keyboardEnabled) {
- attachKeyboardListeners();
- }
- }
-
- /**
- * Detach event listeners to prevent memory leaks
- */
- function detachEventListeners() {
- if (options.swipeEnabled) {
- detachSwipeListeners();
- }
- if (options.keyboardEnabled) {
- detachKeyboardListeners();
- }
- }
-
- /**
- * Touch swipe event listeners
- */
- let swipeStartX = 0;
- let swipeStartY = 0;
-
- function onTouchStart(e) {
- swipeStartX = e.touches[0].clientX;
- swipeStartY = e.touches[0].clientY;
- }
-
- function onTouchEnd(e) {
- const swipeEndX = e.changedTouches[0].clientX;
- const swipeEndY = e.changedTouches[0].clientY;
- const diffX = swipeEndX - swipeStartX;
- const diffY = swipeEndY - swipeStartY;
-
- // Only consider horizontal swipe if delta-x > delta-y
- if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(diffX) > 50) {
- if (diffX > 0) {
- // Swiped right: go to previous
- navigate(-1);
- } else {
- // Swiped left: go to next
- navigate(1);
- }
- }
- }
-
- function attachSwipeListeners() {
- lightboxModal.addEventListener('touchstart', onTouchStart, false);
- lightboxModal.addEventListener('touchend', onTouchEnd, false);
- }
-
- function detachSwipeListeners() {
- lightboxModal.removeEventListener('touchstart', onTouchStart, false);
- lightboxModal.removeEventListener('touchend', onTouchEnd, false);
- }
-
- /**
- * Keyboard event listeners
- */
- function onKeyDown(e) {
- if (!isOpen) return;
-
- switch (e.key) {
- case 'ArrowLeft':
- navigate(-1);
- e.preventDefault();
- break;
- case 'ArrowRight':
- navigate(1);
- e.preventDefault();
- break;
- case 'Escape':
- closeLightbox();
- e.preventDefault();
- break;
- default:
- break;
- }
- }
-
- function attachKeyboardListeners() {
- document.addEventListener('keydown', onKeyDown);
- }
-
- function detachKeyboardListeners() {
- document.removeEventListener('keydown', onKeyDown);
- }
-
- // Expose public API
- window.PhotoUtils = {
- initLightbox,
- extractEXIF,
- openLightbox,
- closeLightbox,
- };
-})();
diff --git a/assets/js/progress-bar.js b/assets/js/progress-bar.js
deleted file mode 100644
index e171f4f..0000000
--- a/assets/js/progress-bar.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * progress-bar.js
- * Reading progress indicator for articles
- */
-
-(function() {
- 'use strict';
-
- const progressBar = document.getElementById('progress-bar');
- if (!progressBar) return;
-
- window.addEventListener('scroll', () => {
- const windowHeight = document.documentElement.scrollHeight - window.innerHeight;
- const scrolled = window.scrollY;
- const progress = windowHeight > 0 ? (scrolled / windowHeight) * 100 : 0;
-
- progressBar.style.width = progress + '%';
- progressBar.setAttribute('aria-valuenow', Math.round(progress));
- }, { passive: true });
-})();
diff --git a/assets/js/reading-progress.js b/assets/js/reading-progress.js
new file mode 100644
index 0000000..ee1192f
--- /dev/null
+++ b/assets/js/reading-progress.js
@@ -0,0 +1,29 @@
+// Reading progress bar for single pages/articles
+(function() {
+ const progressBar = document.getElementById('reading-progress');
+
+ if (!progressBar) return;
+
+ function updateProgress() {
+ const windowHeight = window.innerHeight;
+ const documentHeight = document.documentElement.scrollHeight - windowHeight;
+ const scrollProgress = documentHeight > 0 ? (window.scrollY / documentHeight) * 100 : 0;
+ progressBar.style.width = scrollProgress + '%';
+ }
+
+ // Throttle the scroll event for better performance
+ let ticking = false;
+
+ window.addEventListener('scroll', function() {
+ if (!ticking) {
+ window.requestAnimationFrame(function() {
+ updateProgress();
+ ticking = false;
+ });
+ ticking = true;
+ }
+ }, false);
+
+ // Initial call
+ updateProgress();
+})();
diff --git a/assets/js/scroll-reveal.js b/assets/js/scroll-reveal.js
deleted file mode 100644
index 026d1be..0000000
--- a/assets/js/scroll-reveal.js
+++ /dev/null
@@ -1,40 +0,0 @@
-/**
- * scroll-reveal.js
- * IntersectionObserver for revealing elements on scroll.
- * Adds 90ms stagger delay per sibling index within each reveal-group.
- */
-
-export function initScrollReveal() {
- 'use strict';
-
- const revealElements = document.querySelectorAll('.reveal');
- if (!revealElements.length) return;
-
- const observer = new IntersectionObserver((entries) => {
- entries.forEach((entry) => {
- if (!entry.isIntersecting) return;
-
- const el = entry.target;
- const parent = el.parentElement;
- const siblings = parent
- ? Array.from(parent.querySelectorAll(':scope > .reveal'))
- : [];
- const index = siblings.indexOf(el);
- const delay = index >= 0 ? index * 90 : 0;
-
- el.style.transitionDelay = delay + 'ms';
- el.classList.add('revealed');
-
- // Remove inline delay after transition so hover transitions are unaffected
- const cleanup = () => {
- el.style.transitionDelay = '';
- el.removeEventListener('transitionend', cleanup);
- };
- el.addEventListener('transitionend', cleanup);
-
- observer.unobserve(el);
- });
- }, { threshold: 0.1 });
-
- revealElements.forEach((el) => observer.observe(el));
-}
diff --git a/assets/js/search.js b/assets/js/search.js
new file mode 100644
index 0000000..8fb6262
--- /dev/null
+++ b/assets/js/search.js
@@ -0,0 +1,134 @@
+// Lazy-load search index from JSON file (language-aware)
+async function loadSearchIndex() {
+ if (window.searchIndex) {
+ return window.searchIndex;
+ }
+ try {
+ // Detect current language from URL
+ const isItalian = window.location.pathname.startsWith('/it/');
+ const indexPath = isItalian ? '/it/search-index.json' : '/search-index.json';
+
+ const response = await fetch(indexPath);
+ if (!response.ok) throw new Error('Failed to load search index');
+ window.searchIndex = await response.json();
+ return window.searchIndex;
+ } catch (error) {
+ console.error('Error loading search index:', error);
+ return [];
+ }
+}
+
+// Filter articles by query (case-insensitive, max 5 results)
+function filterArticles(query, articles) {
+ if (!query.trim()) {
+ return [];
+ }
+ const lowerQuery = query.toLowerCase();
+ return articles
+ .filter(article =>
+ article.title.toLowerCase().includes(lowerQuery) ||
+ article.summary.toLowerCase().includes(lowerQuery)
+ )
+ .slice(0, 5);
+}
+
+// Register Alpine.js components
+document.addEventListener('alpine:init', () => {
+ // Desktop search modal component
+ Alpine.data('searchOverlay', () => ({
+ isOpen: false,
+ searchQuery: '',
+ filteredArticles: [],
+ allArticles: [],
+ indexLoaded: false,
+
+ async open() {
+ this.isOpen = true;
+ await this.ensureIndexLoaded();
+ this.$nextTick(() => {
+ const input = this.$el.querySelector('#search-input-desktop');
+ if (input) input.focus();
+ });
+ },
+
+ close() {
+ this.isOpen = false;
+ this.searchQuery = '';
+ this.filteredArticles = [];
+ },
+
+ async ensureIndexLoaded() {
+ if (!this.indexLoaded) {
+ this.allArticles = await loadSearchIndex();
+ this.indexLoaded = true;
+ }
+ },
+
+ filterArticles(query) {
+ this.searchQuery = query;
+ this.filteredArticles = filterArticles(query, this.allArticles);
+ },
+
+ handleEscape(event) {
+ if (event.key === 'Escape') {
+ this.close();
+ }
+ }
+ }));
+
+ // Mobile search component (integrated into hamburger menu)
+ Alpine.data('mobileSearch', () => ({
+ searchQuery: '',
+ filteredArticles: [],
+ allArticles: [],
+ indexLoaded: false,
+
+ async ensureIndexLoaded() {
+ if (!this.indexLoaded) {
+ this.allArticles = await loadSearchIndex();
+ this.indexLoaded = true;
+ }
+ },
+
+ filterArticles(query) {
+ this.searchQuery = query;
+ this.filteredArticles = filterArticles(query, this.allArticles);
+ }
+ }));
+
+ // Refactored 404 page component
+ Alpine.data('notFoundPage', () => ({
+ showEasterEgg: false,
+ searchQuery: '',
+ filteredArticles: [],
+ allArticles: [],
+ indexLoaded: false,
+
+ async init() {
+ await this.ensureIndexLoaded();
+ },
+
+ async ensureIndexLoaded() {
+ if (!this.indexLoaded) {
+ this.allArticles = await loadSearchIndex();
+ this.indexLoaded = true;
+ }
+ },
+
+ filterArticles(query) {
+ this.searchQuery = query;
+ this.filteredArticles = filterArticles(query, this.allArticles);
+ },
+
+ toggleEasterEgg() {
+ this.showEasterEgg = !this.showEasterEgg;
+ },
+
+ goToRandomArticle() {
+ if (this.allArticles.length > 0) {
+ const randomArticle = this.allArticles[Math.floor(Math.random() * this.allArticles.length)];
+ window.location.href = randomArticle.url;
+ }
+ }
+ }));
+});
diff --git a/assets/js/share-sidebar.js b/assets/js/share-sidebar.js
deleted file mode 100644
index 81e5f6c..0000000
--- a/assets/js/share-sidebar.js
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * share-sidebar.js
- * Social sharing sidebar with copy-to-clipboard
- */
-
-export function initShareSidebar() {
- 'use strict';
-
- const sidebar = document.getElementById('share-sidebar');
- const copyBtn = document.getElementById('share-copy');
-
- if (!sidebar) return;
-
- // Share button handlers
- const shareBtns = sidebar.querySelectorAll('.share-btn[data-platform]');
- shareBtns.forEach((btn) => {
- btn.addEventListener('click', () => {
- const platform = btn.getAttribute('data-platform');
- const url = sidebar.getAttribute('data-url');
- const title = sidebar.getAttribute('data-title');
-
- const shareUrls = {
- whatsapp: `https://wa.me/?text=${encodeURIComponent(title + ' ' + url)}`,
- telegram: `https://t.me/share/url?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
- linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(url)}`,
- twitter: `https://twitter.com/intent/tweet?url=${encodeURIComponent(url)}&text=${encodeURIComponent(title)}`,
- facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(url)}`,
- reddit: `https://reddit.com/submit?url=${encodeURIComponent(url)}&title=${encodeURIComponent(title)}`,
- email: `mailto:?subject=${encodeURIComponent(title)}&body=${encodeURIComponent(url)}`,
- };
-
- if (shareUrls[platform]) {
- window.open(shareUrls[platform], '_blank');
- }
- });
- });
-
- // Copy to clipboard
- if (copyBtn) {
- copyBtn.addEventListener('click', () => {
- const url = sidebar.getAttribute('data-url');
- navigator.clipboard.writeText(url).then(() => {
- const originalText = copyBtn.innerHTML;
- copyBtn.innerHTML = '✓';
- setTimeout(() => {
- copyBtn.innerHTML = originalText;
- }, 2000);
- });
- });
- }
-}
diff --git a/assets/js/tag-cloud-spiral.js b/assets/js/tag-cloud-spiral.js
new file mode 100644
index 0000000..bed4645
--- /dev/null
+++ b/assets/js/tag-cloud-spiral.js
@@ -0,0 +1,122 @@
+document.addEventListener('DOMContentLoaded', function () {
+ var containers = document.querySelectorAll('[data-tag-cloud]');
+ if (!containers.length) return;
+
+ Array.prototype.forEach.call(containers, function (container) {
+ if (container.offsetWidth < 400) return;
+
+ var links = Array.prototype.slice.call(
+ container.querySelectorAll('.tag-cloud-link')
+ );
+ if (!links.length) return;
+
+ // Sort descending by weight (biggest first = placed near center)
+ links.sort(function (a, b) {
+ return parseFloat(b.dataset.weight) - parseFloat(a.dataset.weight);
+ });
+
+ // String hash → deterministic angle seed (0..2π)
+ function hashAngle(str) {
+ var h = 0;
+ for (var i = 0; i < str.length; i++) {
+ h = (h * 31 + str.charCodeAt(i)) & 0xffffffff;
+ }
+ return ((h >>> 0) / 0xffffffff) * 2 * Math.PI;
+ }
+
+ // AABB collision check
+ function overlaps(a, b) {
+ return !(
+ a.right < b.left ||
+ a.left > b.right ||
+ a.bottom < b.top ||
+ a.top > b.bottom
+ );
+ }
+
+ var placed = [];
+ var containerWidth = container.offsetWidth;
+ var cx = containerWidth / 2;
+
+ // Measure each tag before repositioning
+ var sizes = links.map(function (link) {
+ var rect = link.getBoundingClientRect();
+ return { w: rect.width, h: rect.height };
+ });
+
+ // Switch container to relative positioning and remove flex layout
+ container.style.position = 'relative';
+ container.style.display = 'block';
+ container.classList.remove('flex', 'flex-wrap');
+
+ var padding = -2; // px gap between tags (negative allows ~2px edge overlap)
+ var aStep = 0.2; // radians per spiral step
+ var rScale = (containerWidth * 0.013); // spiral tightness
+
+ var minTop = Infinity, maxBottom = -Infinity;
+
+ links.forEach(function (link, i) {
+ var w = sizes[i].w;
+ var h = sizes[i].h;
+ var seed = hashAngle(link.href);
+ var theta = seed;
+ var placed_rect;
+
+ // Step along spiral until no collision
+ for (var attempt = 0; attempt < 3000; attempt++) {
+ var r = rScale * theta;
+ var x = cx + r * Math.cos(theta) - w / 2;
+ var y = r * Math.sin(theta) - h / 2;
+
+ var candidate = { left: x, top: y, right: x + w, bottom: y + h };
+ var collision = false;
+
+ for (var j = 0; j < placed.length; j++) {
+ var p = placed[j];
+ var padded = {
+ left: p.left - padding,
+ top: p.top - padding,
+ right: p.right + padding,
+ bottom: p.bottom + padding
+ };
+ if (overlaps(candidate, padded)) {
+ collision = true;
+ break;
+ }
+ }
+
+ if (!collision) {
+ placed_rect = candidate;
+ break;
+ }
+ theta += aStep;
+ }
+
+ if (!placed_rect) {
+ // Fallback: just append to flow if spiral exhausted
+ link.style.position = 'static';
+ return;
+ }
+
+ placed.push(placed_rect);
+
+ link.style.position = 'absolute';
+ link.style.left = Math.round(placed_rect.left) + 'px';
+ link.style.top = Math.round(placed_rect.top) + 'px';
+
+ if (placed_rect.top < minTop) minTop = placed_rect.top;
+ if (placed_rect.bottom > maxBottom) maxBottom = placed_rect.bottom;
+ });
+
+ // Normalize: shift all tags so topmost is at y=16px
+ var offset = 16 - minTop;
+ links.forEach(function (link) {
+ if (link.style.position === 'absolute') {
+ link.style.top = (parseInt(link.style.top) + offset) + 'px';
+ }
+ });
+
+ // Set container height to fit all tags + 2rem bottom padding (32px)
+ container.style.height = (maxBottom - minTop + 48) + 'px';
+ });
+});
diff --git a/assets/js/theme-toggle.js b/assets/js/theme-toggle.js
index e03fce7..bb95b2a 100644
--- a/assets/js/theme-toggle.js
+++ b/assets/js/theme-toggle.js
@@ -1,87 +1,53 @@
-// theme-toggle.js
-(function() {
- const STORAGE_KEY = 'danix-theme';
- const DARK_CLASS = 'theme-dark';
- const LIGHT_CLASS = 'theme-light';
-
- // Initialize theme on page load
- function init() {
- const saved = localStorage.getItem(STORAGE_KEY);
- const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
- const isDark = saved === null ? prefersDark : saved === 'dark';
-
- applyTheme(isDark ? 'dark' : 'light');
+document.addEventListener('DOMContentLoaded', function() {
+ const themeToggle = document.getElementById('theme-toggle');
+ const sunIcon = document.getElementById('theme-icon-sun');
+ const moonIcon = document.getElementById('theme-icon-moon');
+
+ function updateThemeIcon() {
+ const isDark = document.documentElement.classList.contains('theme-dark');
+ if (sunIcon && moonIcon) {
+ if (isDark) {
+ sunIcon.style.display = 'block';
+ moonIcon.style.display = 'none';
+ } else {
+ sunIcon.style.display = 'none';
+ moonIcon.style.display = 'block';
+ }
+ }
}
- // Apply theme to document
- function applyTheme(theme) {
- const html = document.documentElement;
-
- html.classList.remove(DARK_CLASS, LIGHT_CLASS);
-
- if (theme === 'dark') {
- html.classList.remove(LIGHT_CLASS);
- localStorage.setItem(STORAGE_KEY, 'dark');
- } else {
- html.classList.add(LIGHT_CLASS);
- localStorage.setItem(STORAGE_KEY, 'light');
- }
+ // Update icon on initial load
+ if (sunIcon && moonIcon) {
+ updateThemeIcon();
}
- // Get current theme
- function getCurrentTheme() {
- return document.documentElement.classList.contains(LIGHT_CLASS) ? 'light' : 'dark';
+ if (!themeToggle) {
+ return;
}
- // Toggle theme
- function toggleTheme() {
- const current = getCurrentTheme();
- const next = current === 'dark' ? 'light' : 'dark';
- applyTheme(next);
+ themeToggle.addEventListener('click', function(e) {
+ e.preventDefault();
- // Dispatch custom event for other scripts to listen
- window.dispatchEvent(new CustomEvent('theme-changed', { detail: { theme: next } }));
- }
+ // Get current theme from html element
+ const htmlElement = document.documentElement;
+ const isDark = htmlElement.classList.contains('theme-dark');
+ const newTheme = isDark ? 'light' : 'dark';
- // Setup toggle button
- function setupToggleButton() {
- const btn = document.getElementById('theme-switch');
- if (btn) {
- btn.addEventListener('click', toggleTheme);
- updateToggleButtonUI();
+ // Remove both theme classes
+ htmlElement.classList.remove('theme-light', 'theme-dark');
- // Listen for theme changes to update button UI
- window.addEventListener('theme-changed', updateToggleButtonUI);
- }
- }
+ // Add the new theme class
+ htmlElement.classList.add(`theme-${newTheme}`);
- function updateToggleButtonUI() {
- const btn = document.getElementById('theme-switch');
- if (btn) {
- const current = getCurrentTheme();
- if (current === 'light') {
- btn.classList.add('light');
- } else {
- btn.classList.remove('light');
- }
- }
- }
+ // Persist to localStorage
+ localStorage.setItem('theme', newTheme);
- // Initialize on DOMContentLoaded
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', function() {
- init();
- setupToggleButton();
- });
- } else {
- init();
- setupToggleButton();
- }
+ // Update icon display
+ updateThemeIcon();
- // Expose to global scope for testing
- window.ThemeToggle = {
- toggle: toggleTheme,
- set: applyTheme,
- get: getCurrentTheme,
- };
-})();
+ // Update Feather Icons if available
+ if (window.feather) {
+ window.feather.replace();
+ }
+ });
+});
diff --git a/assets/js/typing.js b/assets/js/typing.js
deleted file mode 100644
index 369fed7..0000000
--- a/assets/js/typing.js
+++ /dev/null
@@ -1,69 +0,0 @@
-/**
- * typing.js
- * Typing animation for .hero-role / #typed element
- */
-
-export function initTyping() {
- 'use strict';
-
- const typedElement = document.getElementById('typed');
- if (!typedElement) return;
-
- const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
- const phrasesJson = typedElement.getAttribute('data-phrases');
- let phrases = [];
-
- if (phrasesJson) {
- try {
- phrases = JSON.parse(phrasesJson);
- } catch (e) {
- console.warn('Failed to parse typing phrases:', e);
- phrases = ['Security & Web Dev', 'WordPress Developer'];
- }
- }
-
- if (!phrases.length) return;
-
- let currentPhraseIndex = 0;
- let currentCharIndex = 0;
- let isDeleting = false;
-
- function type() {
- const phrase = phrases[currentPhraseIndex];
- const speed = isDeleting ? 50 : 100;
-
- if (isDeleting) {
- currentCharIndex--;
- } else {
- currentCharIndex++;
- }
-
- typedElement.textContent = phrase.substring(0, currentCharIndex);
-
- // Add cursor
- if (!isDeleting && currentCharIndex === phrase.length) {
- typedElement.innerHTML += '<span class="cursor"></span>';
- } else if (isDeleting && currentCharIndex === 0) {
- currentPhraseIndex = (currentPhraseIndex + 1) % phrases.length;
- isDeleting = false;
- setTimeout(type, 500);
- return;
- } else {
- typedElement.innerHTML = phrase.substring(0, currentCharIndex) + '<span class="cursor"></span>';
- }
-
- if (!isDeleting && currentCharIndex === phrase.length) {
- isDeleting = true;
- setTimeout(type, 2000);
- } else {
- setTimeout(type, prefersReducedMotion ? 0 : speed);
- }
- }
-
- if (prefersReducedMotion) {
- // Just show the first phrase without animation
- typedElement.textContent = phrases[0];
- } else {
- type();
- }
-}
diff --git a/assets/jsconfig.json b/assets/jsconfig.json
deleted file mode 100644
index 377218c..0000000
--- a/assets/jsconfig.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "compilerOptions": {
- "baseUrl": ".",
- "paths": {
- "*": [
- "*"
- ]
- }
- }
-} \ No newline at end of file
diff --git a/content/_index.md b/content/_index.md
deleted file mode 100644
index f04efe2..0000000
--- a/content/_index.md
+++ /dev/null
@@ -1,6 +0,0 @@
-+++
-title = "Home"
-date = 2026-01-01T00:00:00Z
-+++
-
-This is the home page content. The landing page layout will use this, but typically you won't add much here — the hero and latest posts grid will be the main focus.
diff --git a/content/articles/_index.md b/content/articles/_index.md
deleted file mode 100644
index 3d43a65..0000000
--- a/content/articles/_index.md
+++ /dev/null
@@ -1,6 +0,0 @@
-+++
-title = "Articles"
-date = 2026-01-01T00:00:00Z
-+++
-
-Browse all articles, organized by type. Use the filters to find what you're looking for.
diff --git a/content/is/_index.md b/content/is/_index.md
deleted file mode 100644
index 391d6d1..0000000
--- a/content/is/_index.md
+++ /dev/null
@@ -1,8 +0,0 @@
-+++
-title = "About"
-date = 2026-01-01T00:00:00Z
-+++
-
-Hi, I'm Danilo. I write about tech, life, and the things that matter.
-
-This is your about page. Edit this to tell your story.
diff --git a/content/is/here.md b/content/is/here.md
deleted file mode 100644
index 443950a..0000000
--- a/content/is/here.md
+++ /dev/null
@@ -1,87 +0,0 @@
-+++
-title = "Contact"
-date = 2026-01-01T00:00:00Z
-slug = "here"
-+++
-
-Get in touch — send me a message and I'll get back to you soon.
-
-<form method="POST" action="/api/contact.php" class="contact-form">
- <div class="form-group">
- <label for="name">Name</label>
- <input type="text" id="name" name="name" required>
- </div>
-
- <div class="form-group">
- <label for="email">Email</label>
- <input type="email" id="email" name="email" required>
- </div>
-
- <div class="form-group">
- <label for="subject">Subject</label>
- <input type="text" id="subject" name="subject" required>
- </div>
-
- <div class="form-group">
- <label for="message">Message</label>
- <textarea id="message" name="message" rows="6" required></textarea>
- </div>
-
- <button type="submit" class="form-submit">Send</button>
-</form>
-
-<style>
- .contact-form {
- max-width: 500px;
- margin: 2rem 0;
- }
-
- .form-group {
- margin-bottom: 1.5rem;
- }
-
- .form-group label {
- display: block;
- font-weight: 600;
- margin-bottom: 0.5rem;
- color: var(--text);
- }
-
- .form-group input,
- .form-group textarea {
- width: 100%;
- padding: 0.75rem;
- border: 1px solid var(--border);
- border-radius: 4px;
- background: var(--surface);
- color: var(--text);
- font-family: var(--font-body);
- font-size: 1rem;
- }
-
- .form-group input:focus,
- .form-group textarea:focus {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 12px rgba(168, 85, 247, 0.3);
- }
-
- .form-submit {
- padding: 0.75rem 2rem;
- background: var(--accent);
- color: #fff;
- border: none;
- border-radius: 4px;
- font-family: var(--font-mono);
- font-size: var(--fs-btn);
- text-transform: uppercase;
- letter-spacing: 0.1em;
- cursor: pointer;
- transition: var(--transition);
- }
-
- .form-submit:hover {
- background: var(--accent2);
- color: var(--bg);
- }
-</style>
diff --git a/content/posts/_index.md b/content/posts/_index.md
deleted file mode 100644
index e7066c0..0000000
--- a/content/posts/_index.md
+++ /dev/null
@@ -1,7 +0,0 @@
-+++
-title = 'Posts'
-date = 2023-01-01T08:30:00-07:00
-draft = false
-+++
-
-Tempor est exercitation ad qui pariatur quis adipisicing aliquip nisi ea consequat ipsum occaecat. Nostrud consequat ullamco laboris fugiat esse esse adipisicing velit laborum ipsum incididunt ut enim. Dolor pariatur nulla quis fugiat dolore excepteur. Aliquip ad quis aliqua enim do consequat.
diff --git a/content/posts/post-1.md b/content/posts/post-1.md
deleted file mode 100644
index 3e3fc6b..0000000
--- a/content/posts/post-1.md
+++ /dev/null
@@ -1,10 +0,0 @@
-+++
-title = 'Post 1'
-date = 2023-01-15T09:00:00-07:00
-draft = false
-tags = ['red']
-+++
-
-Tempor proident minim aliquip reprehenderit dolor et ad anim Lorem duis sint eiusmod. Labore ut ea duis dolor. Incididunt consectetur proident qui occaecat incididunt do nisi Lorem. Tempor do laborum elit laboris excepteur eiusmod do. Eiusmod nisi excepteur ut amet pariatur adipisicing Lorem.
-
-Occaecat nulla excepteur dolore excepteur duis eiusmod ullamco officia anim in voluptate ea occaecat officia. Cillum sint esse velit ea officia minim fugiat. Elit ea esse id aliquip pariatur cupidatat id duis minim incididunt ea ea. Anim ut duis sunt nisi. Culpa cillum sit voluptate voluptate eiusmod dolor. Enim nisi Lorem ipsum irure est excepteur voluptate eu in enim nisi. Nostrud ipsum Lorem anim sint labore consequat do.
diff --git a/content/posts/post-2.md b/content/posts/post-2.md
deleted file mode 100644
index 22b8287..0000000
--- a/content/posts/post-2.md
+++ /dev/null
@@ -1,10 +0,0 @@
-+++
-title = 'Post 2'
-date = 2023-02-15T10:00:00-07:00
-draft = false
-tags = ['red','green']
-+++
-
-Anim eiusmod irure incididunt sint cupidatat. Incididunt irure irure irure nisi ipsum do ut quis fugiat consectetur proident cupidatat incididunt cillum. Dolore voluptate occaecat qui mollit laborum ullamco et. Ipsum laboris officia anim laboris culpa eiusmod ex magna ex cupidatat anim ipsum aute. Mollit aliquip occaecat qui sunt velit ut cupidatat reprehenderit enim sunt laborum. Velit veniam in officia nulla adipisicing ut duis officia.
-
-Exercitation voluptate irure in irure tempor mollit Lorem nostrud ad officia. Velit id fugiat occaecat do tempor. Sit officia Lorem aliquip eu deserunt consectetur. Aute proident deserunt in nulla aliquip dolore ipsum Lorem ut cupidatat consectetur sit sint laborum. Esse cupidatat sit sint sunt tempor exercitation deserunt. Labore dolor duis laborum est do nisi ut veniam dolor et nostrud nostrud.
diff --git a/content/posts/post-3/bryce-canyon.jpg b/content/posts/post-3/bryce-canyon.jpg
deleted file mode 100644
index 9a923be..0000000
--- a/content/posts/post-3/bryce-canyon.jpg
+++ /dev/null
Binary files differ
diff --git a/content/posts/post-3/index.md b/content/posts/post-3/index.md
deleted file mode 100644
index ca42a66..0000000
--- a/content/posts/post-3/index.md
+++ /dev/null
@@ -1,12 +0,0 @@
-+++
-title = 'Post 3'
-date = 2023-03-15T11:00:00-07:00
-draft = false
-tags = ['red','green','blue']
-+++
-
-Occaecat aliqua consequat laborum ut ex aute aliqua culpa quis irure esse magna dolore quis. Proident fugiat labore eu laboris officia Lorem enim. Ipsum occaecat cillum ut tempor id sint aliqua incididunt nisi incididunt reprehenderit. Voluptate ad minim sint est aute aliquip esse occaecat tempor officia qui sunt. Aute ex ipsum id ut in est velit est laborum incididunt. Aliqua qui id do esse sunt eiusmod id deserunt eu nostrud aute sit ipsum. Deserunt esse cillum Lorem non magna adipisicing mollit amet consequat.
-
-![Bryce Canyon National Park](bryce-canyon.jpg)
-
-Sit excepteur do velit veniam mollit in nostrud laboris incididunt ea. Amet eu cillum ut reprehenderit culpa aliquip labore laborum amet sit sit duis. Laborum id proident nostrud dolore laborum reprehenderit quis mollit nulla amet veniam officia id id. Aliquip in deserunt qui magna duis qui pariatur officia sunt deserunt.
diff --git a/hugo.toml b/hugo.toml
index e5fdd95..e69de29 100644
--- a/hugo.toml
+++ b/hugo.toml
@@ -1,60 +0,0 @@
-baseURL = 'https://danix.xyz/'
-title = 'danilo m.'
-locale = 'en-US'
-
-# Sections
-[outputs]
- home = ['HTML', 'RSS']
- section = ['HTML']
- page = ['HTML']
-
-[markup]
- [markup.highlight]
- codeFences = true
- lineNos = false
- tabWidth = 2
- # Style: monokai - customize colors in layout files and CSS
- style = 'monokai'
- noClasses = false
- [markup.tableOfContents]
- endLevel = 3
- ordered = false
- startLevel = 2
-
-# Navigation Menu
-# Add menu items below. Items are displayed left-to-right in order of weight.
-# Parameters:
-# name = Text displayed in menu (visible to users)
-# pageRef = Path to your page (e.g., /articles, /is/about)
-# weight = Order (lower numbers = leftmost, higher = rightmost)
-#
-# For external links, use 'url' instead of 'pageRef':
-# url = 'https://example.com'
-
-[[menu.main]]
- name = 'articles'
- pageRef = '/articles'
- weight = 10
-
-[[menu.main]]
- name = 'about'
- pageRef = '/is'
- weight = 20
-
-[[menu.main]]
- name = 'contact'
- pageRef = '/is/here'
- weight = 30
-
-# Example: Add a new page to the menu
-# [[menu.main]]
-# name = 'uses'
-# pageRef = '/is/uses'
-# weight = 40
-
-# Content sections
-[params]
- description = 'Writing about IT, life, and the things that matter.'
- author = 'Danilo M.'
- avatar = 'DM' # Can be image path or initials
- typingPhrases = ['Security & Web Dev', 'WordPress Developer', 'Bash Enthusiast', 'Slackware Admin']
diff --git a/i18n/en.yaml b/i18n/en.yaml
new file mode 100644
index 0000000..23b7b6b
--- /dev/null
+++ b/i18n/en.yaml
@@ -0,0 +1,171 @@
+# Navigation & UI
+home: "Home"
+articles: "Articles"
+about: "About"
+here: "Contact"
+repo: "Repo"
+legal: "Privacy"
+language: "Language"
+toggleTheme: "Theme"
+toggleMenu: "Menu"
+closeMenu: "Close"
+skipToContent: "Skip to main content"
+email: "Email"
+contact: "Contact"
+links: "Links"
+allRightsReserved: "All rights reserved."
+footer_built_with: "built with"
+footer_features: "features"
+footer_made_with: "Made with"
+footer_lack_of: "lack of"
+footer_lots_of: "lots of"
+footer_by: "by"
+footer_love: "love"
+footer_sleep: "sleep"
+footer_coffee: "coffee"
+footer_about_name: "Name"
+footer_about_role: "role"
+footer_about_cert: "cert"
+footer_about_os: "os"
+footer_about_focus: "focus"
+footer_about_role_value: "Cybersecurity Specialist"
+footer_about_os_value: "Slackware"
+footer_about_os_year: "2005–present"
+footer_about_focus_value: "open-source · privacy"
+searchArticles: "Search Articles"
+searchPlaceholder: "Search by title or content..."
+noSearchResults: "No articles found matching your search."
+
+# Articles
+readMore: "Read more"
+published: "Published"
+updated: "Updated"
+readingTime: "reading time"
+min: "min"
+author: "Author"
+category: "Category"
+categories: "Categories"
+tag: "Tag"
+tags: "Tags"
+relatedPosts: "Related articles"
+noRelated: "No related articles."
+postCount:
+ one: "1 post"
+ other: "{{ . }} posts"
+
+# Article types
+life: "Life"
+photo: "Photo"
+link: "Link"
+quote: "Quote"
+tech: "Tech"
+
+# Sharing
+share: "Share"
+shareOn: "Share on"
+copyLink: "Copy link"
+twitter: "Twitter"
+facebook: "Facebook"
+reddit: "Reddit"
+pinterest: "Pinterest"
+whatsapp: "WhatsApp"
+telegram: "Telegram"
+signal: "Signal"
+shareViaEmail: "Share via email"
+linkCopied: "Link copied!"
+back_to_top: "Back to top"
+
+# Forms
+name: "Name"
+message: "Message"
+submit: "Send"
+sending: "Sending..."
+success: "Message sent successfully!"
+error: "An error occurred. Please try again."
+
+# Form Components
+form_invalid_email: "Please enter a valid email address"
+form_password_help: "Must be at least 8 characters"
+form_agree_terms: "I agree to the terms and conditions"
+form_select_interests: "Select your interests"
+form_interest_tech: "Technology"
+form_interest_design: "Design"
+form_select_preference: "Select a preference"
+form_option_a: "Option A"
+form_option_b: "Option B"
+form_first_name: "First Name"
+form_last_name: "Last Name"
+form_search: "Search"
+form_search_btn: "Search"
+form_open_alert: "Open Alert Modal"
+form_open_confirm: "Open Confirm Modal"
+form_open_content: "Open Content Modal"
+form_alert_title: "Alert"
+form_alert_message: "This is an alert modal. Click OK to dismiss."
+form_ok: "OK"
+form_confirm_title: "Confirm Action"
+form_confirm_message: "Are you sure you want to continue?"
+form_cancel: "Cancel"
+form_confirm: "Confirm"
+form_content_title: "Modal with Content"
+form_content_message: "This modal contains detailed content. You can add forms, lists, or any HTML here."
+form_close: "Close"
+form_save: "Save"
+
+# Social
+follow: "Follow me"
+contactMe: "Contact me"
+
+# 404 Page
+notFound:
+ other: "Page Not Found"
+notFoundHeading:
+ other: "404"
+notFoundMessage:
+ other: "Sorry, the page you're looking for doesn't exist. Try searching or browse the articles below."
+searchButton:
+ other: "Search"
+recentArticles:
+ other: "Recent Articles"
+goHome:
+ other: "Go Home"
+browseArticles:
+ other: "Browse Articles"
+contactSupport:
+ other: "Get in Touch"
+followWhiteRabbit:
+ other: "Follow the white rabbit"
+bluePill:
+ other: "Stay Here"
+redPill:
+ other: "Show Me More"
+easterEggTitle:
+ other: "Choose Your Path"
+
+# Repository Page
+repositoryTitle:
+ other: "Slackware Repository"
+repositorySubtitle:
+ other: "Download and install my curated Slackware packages"
+quickStartTitle:
+ other: "Quick Start"
+copyCommand:
+ other: "Copy"
+copiedMessage:
+ other: "Copied to clipboard!"
+installationTitle:
+ other: "Installation"
+usageTitle:
+ other: "Usage"
+availablePackagesTitle:
+ other: "Available Packages"
+githubReposTitle:
+ other: "GitHub SlackBuild Repositories"
+visitGithub:
+ other: "Visit GitHub"
+repo_example_1_desc:
+ other: "SlackBuild scripts for Example Package 1. Build system and tools for Slackware package management."
+repo_example_2_desc:
+ other: "SlackBuild scripts for Example Package 2. A comprehensive package with full documentation and support."
+repo_example_3_desc:
+ other: "SlackBuild scripts for Example Package 3. Utility package for system administration and configuration."
diff --git a/i18n/it.yaml b/i18n/it.yaml
new file mode 100644
index 0000000..8606ffa
--- /dev/null
+++ b/i18n/it.yaml
@@ -0,0 +1,173 @@
+# Navigation & UI
+home: "Home"
+articles: "Articoli"
+about: "Chi Sono"
+here: "Contatti"
+repo: "Repo"
+legal: "Privacy"
+language: "Lingua"
+toggleTheme: "Tema"
+toggleMenu: "Menu"
+closeMenu: "Chiudi"
+skipToContent: "Salta al contenuto principale"
+email: "Email"
+contact: "Contatti"
+links: "Link"
+allRightsReserved: "Tutti i diritti riservati."
+footer_built_with: "costruito con"
+footer_features: "caratteristiche"
+footer_made_with: "Fatto con"
+footer_lack_of: "mancanza di"
+footer_lots_of: "molti"
+footer_by: "da"
+footer_love: "amore"
+footer_sleep: "sonno"
+footer_coffee: "caffè"
+footer_about_name: "Nome"
+footer_about_role: "ruolo"
+footer_about_cert: "cert"
+footer_about_os: "os"
+footer_about_focus: "focus"
+footer_about_role_value: "Specialista in Cybersecurity"
+footer_about_os_value: "Slackware"
+footer_about_os_year: "2005–presente"
+footer_about_focus_value: "open-source · privacy"
+searchArticles: "Cerca Articoli"
+searchPlaceholder: "Cerca per titolo o contenuto..."
+noSearchResults: "Nessun articolo trovato per la tua ricerca."
+
+# Articles
+readMore: "Continua a leggere"
+published: "Pubblicato"
+updated: "Aggiornato"
+readingTime: "tempo di lettura"
+min: "min"
+author: "Autore"
+category: "Categoria"
+categories: "Categorie"
+tag: "Tag"
+tags: "Tag"
+relatedPosts: "Articoli correlati"
+noRelated: "Nessun articolo correlato."
+postCount:
+ one: "1 articolo"
+ other: "{{ . }} articoli"
+
+# Article types
+life: "Vita"
+photo: "Foto"
+link: "Link"
+quote: "Citazioni"
+tech: "Tech"
+
+# Sharing
+share: "Condividi"
+shareOn: "Condividi su"
+copyLink: "Copia link"
+twitter: "Twitter"
+facebook: "Facebook"
+reddit: "Reddit"
+pinterest: "Pinterest"
+whatsapp: "WhatsApp"
+telegram: "Telegram"
+signal: "Signal"
+shareViaEmail: "Condividi via email"
+linkCopied: "Link copiato!"
+back_to_top: "Torna in cima"
+
+# Forms
+name: "Nome"
+message: "Messaggio"
+submit: "Invia"
+sending: "Invio in corso..."
+success: "Messaggio inviato con successo!"
+error: "Si è verificato un errore. Riprova."
+
+# Form Components
+form_invalid_email: "Inserire un indirizzo email valido"
+form_password_help: "Deve avere almeno 8 caratteri"
+form_agree_terms: "Accetto i termini e le condizioni"
+form_select_interests: "Seleziona i tuoi interessi"
+form_interest_tech: "Tecnologia"
+form_interest_design: "Design"
+form_select_preference: "Seleziona una preferenza"
+form_option_a: "Opzione A"
+form_option_b: "Opzione B"
+form_first_name: "Nome"
+form_last_name: "Cognome"
+form_search: "Cerca"
+form_search_btn: "Cerca"
+form_open_alert: "Apri modale di avviso"
+form_open_confirm: "Apri modale di conferma"
+form_open_content: "Apri modale contenuto"
+form_alert_title: "Avviso"
+form_alert_message: "Questo è un modale di avviso. Fai clic su OK per chiudere."
+form_ok: "OK"
+form_confirm_title: "Conferma azione"
+form_confirm_message: "Sei sicuro di voler continuare?"
+form_cancel: "Annulla"
+form_confirm: "Conferma"
+form_content_title: "Modale con contenuto"
+form_content_message: "Questo modale contiene contenuti dettagliati. Puoi aggiungere moduli, elenchi o qualsiasi HTML qui."
+form_close: "Chiudi"
+form_save: "Salva"
+
+# Social
+follow: "Seguimi"
+contactMe: "Contattami"
+
+# 404 Page
+notFound:
+ other: "Pagina Non Trovata"
+notFoundHeading:
+ other: "404"
+notFoundMessage:
+ other: "Mi dispiace, la pagina che stai cercando non esiste. Prova a cercare o sfoglia gli articoli qui sotto."
+searchButton:
+ other: "Cerca"
+recentArticles:
+ other: "Articoli Recenti"
+goHome:
+ other: "Torna a Casa"
+browseArticles:
+ other: "Sfoglia Articoli"
+contactSupport:
+ other: "Contattami"
+followWhiteRabbit:
+ other: "Segui il coniglio bianco"
+bluePill:
+ other: "Rimani Qui"
+redPill:
+ other: "Mostrami di Più"
+easterEggTitle:
+ other: "Scegli il Tuo Percorso"
+
+# Repository Page
+repositoryTitle:
+ other: "Repository Slackware"
+repositorySubtitle:
+ other: "Scarica e installa i miei pacchetti Slackware curati"
+quickStartTitle:
+ other: "Inizio Rapido"
+copyCommand:
+ other: "Copia"
+copiedMessage:
+ other: "Copiato negli appunti!"
+installationTitle:
+ other: "Installazione"
+usageTitle:
+ other: "Utilizzo"
+availablePackagesTitle:
+ other: "Pacchetti Disponibili"
+githubReposTitle:
+ other: "Repository GitHub SlackBuild"
+visitGithub:
+ other: "Visita GitHub"
+
+# Repository Descriptions
+repo_example_1_desc:
+ other: "Script SlackBuild per Pacchetto Esempio 1. Sistema di compilazione e strumenti per la gestione dei pacchetti Slackware."
+repo_example_2_desc:
+ other: "Script SlackBuild per Pacchetto Esempio 2. Un pacchetto completo con documentazione e supporto completi."
+repo_example_3_desc:
+ other: "Script SlackBuild per Pacchetto Esempio 3. Pacchetto di utilità per amministrazione di sistema e configurazione."
diff --git a/layouts/404.en.html b/layouts/404.en.html
new file mode 100644
index 0000000..2829a20
--- /dev/null
+++ b/layouts/404.en.html
@@ -0,0 +1,142 @@
+{{ define "main" }}
+
+<main class="min-h-screen px-4 py-12">
+ <div class="mx-auto px-4 py-12 max-w-4xl border border-border glow-accent rounded-lg bg-bg p-8" x-data="notFoundPage()">
+ <div class="text-center">
+ <!-- 404 Heading -->
+ <h1 class="text-7xl md:text-8xl font-bold text-accent mb-4 animate-fade-in">
+ 404
+ </h1>
+
+ <!-- Error Message -->
+ <h2 class="text-3xl md:text-4xl font-bold mb-6">
+ {{ i18n "notFound" }}
+ </h2>
+
+ <p class="text-lg text-text-dim mb-8">
+ {{ i18n "notFoundMessage" }}
+ </p>
+
+ <!-- Search Box -->
+ <div class="mb-12">
+ <form id="search-form" class="flex flex-col gap-4">
+ <label for="search-input" class="sr-only">{{ i18n "searchPlaceholder" }}</label>
+ <input
+ id="search-input"
+ type="text"
+ placeholder="{{ (i18n "searchPlaceholder") }}"
+ class="px-4 py-3 border-2 border-border rounded focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent bg-bg text-text"
+ @input="filterArticles($el.value)"
+ />
+ </form>
+ <div id="search-results" class="mt-4 text-left space-y-3" x-show="filteredArticles.length > 0">
+ <template x-for="article in filteredArticles" :key="article.title">
+ <div class="p-4 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors">
+ <a :href="article.url" class="block text-left">
+ <h4 class="font-bold text-accent hover:underline" x-text="article.title"></h4>
+ <p class="text-sm text-text-dim mt-1" x-text="article.date"></p>
+ </a>
+ </div>
+ </template>
+ </div>
+ <div x-show="searchQuery && filteredArticles.length === 0" class="mt-4 text-text-dim">
+ {{ i18n "noSearchResults" }}
+ </div>
+ </div>
+
+ <!-- Recent Articles Section -->
+ <div class="mb-12">
+ <h3 class="text-2xl font-bold mb-6">{{ i18n "recentArticles" }}</h3>
+ <div class="space-y-4">
+ {{ range first 5 (where .Site.RegularPages "Section" "articles") }}
+ <div class="p-4 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors">
+ <a href="{{ .Permalink }}" class="block text-left">
+ <h4 class="font-bold text-accent hover:underline">{{ .Title }}</h4>
+ <p class="text-sm text-text-dim mt-1">
+ {{ .Date.Format "Jan 02, 2006" }}
+ </p>
+ </a>
+ </div>
+ {{ end }}
+ </div>
+ </div>
+
+ <!-- Explore Topics -->
+ <div class="mb-12 text-left">
+ {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h3") }}
+ </div>
+
+ <!-- Navigation Links -->
+ <div class="space-y-4 flex flex-col items-center mb-12">
+ <a href="/" class="btn btn-primary">
+ {{ i18n "goHome" }}
+ </a>
+ <a href="/articles/" class="btn btn-secondary">
+ {{ i18n "browseArticles" }}
+ </a>
+ <a href="/is/here/" class="btn btn-outline">
+ {{ i18n "contactSupport" }}
+ </a>
+ </div>
+
+ <!-- Easter Egg Trigger -->
+ <div class="mt-12 pt-8 border-t border-border">
+ <button
+ type="button"
+ @click="toggleEasterEgg()"
+ class="text-sm text-text-dim hover:text-accent transition-colors underline"
+ >
+ {{ i18n "followWhiteRabbit" }}
+ </button>
+ </div>
+
+ <!-- Easter Egg Modal (Hidden by default) -->
+ <div
+ class="fixed inset-0 z-50"
+ :class="{ 'flex items-center justify-center': showEasterEgg, 'hidden': !showEasterEgg }"
+ x-show="showEasterEgg"
+ x-cloak
+ >
+ <!-- Overlay -->
+ <div
+ class="absolute inset-0 bg-black/50"
+ @click="showEasterEgg = false"
+ ></div>
+
+ <!-- Modal Content -->
+ <div class="relative bg-bg border-2 border-accent p-8 rounded-lg shadow-xl max-w-md mx-4">
+ <h2 class="text-2xl font-bold mb-6 text-accent">{{ i18n "easterEggTitle" }}</h2>
+
+ <div class="space-y-4">
+ <button
+ type="button"
+ @click="showEasterEgg = false; window.location.href = '{{ .Site.BaseURL }}'"
+ class="w-full btn btn-primary"
+ >
+ 💊 {{ i18n "bluePill" }}
+ </button>
+
+ <button
+ type="button"
+ @click="goToRandomArticle()"
+ class="w-full btn btn-secondary"
+ >
+ 🐰 {{ i18n "redPill" }}
+ </button>
+ </div>
+
+ <button
+ type="button"
+ @click="showEasterEgg = false"
+ class="absolute top-4 right-4 text-text-dim hover:text-text dark:hover:text-text transition-colors"
+ aria-label="Close modal"
+ >
+ ✕
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</main>
+
+{{ end }}
diff --git a/layouts/404.html b/layouts/404.html
deleted file mode 100644
index 3295630..0000000
--- a/layouts/404.html
+++ /dev/null
@@ -1,98 +0,0 @@
-<!DOCTYPE html>
-<html lang="{{ .Site.Language.Locale }}" dir="{{ or .Site.Language.Direction `ltr` }}">
-<head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <meta name="theme-color" content="#060b10">
- <title>404 — {{ .Site.Title }}</title>
-
- <script>
- (function() {
- const theme = localStorage.getItem('theme') || 'dark';
- if (theme === 'light') {
- document.documentElement.classList.add('theme-light');
- }
- })();
- </script>
-
- {{ partialCached "head/css.html" . }}
- {{ partialCached "head/js.html" . }}
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.css">
- {{ $notfound := resources.Get "css/components/404.css" | fingerprint }}
- <link rel="stylesheet" href="{{ $notfound.RelPermalink }}">
-</head>
-<body>
- <a href="#main-content" class="skip-link">Skip to content</a>
- <canvas id="matrix-canvas" aria-hidden="true" data-mode="background"></canvas>
-
- {{ partial "header.html" . }}
-
- <main id="main-content" class="hero--404">
- <div class="content-wrapper">
- <!-- Left: Quote + Search -->
- <div class="hero-left-404">
- <div class="quote-section">
- <div class="quote-mark">"</div>
- <p class="quote-text" id="quote-text">The page you're looking for doesn't exist. But that's okay, nothing exists until you find it.</p>
- <p class="quote-author" id="quote-author">— 404 Philosopher</p>
- </div>
-
- <div class="search-box">
- <input type="text" placeholder="Search articles..." aria-label="Search">
- <button aria-label="Search">
- <svg data-feather="search"></svg>
- </button>
- </div>
-
- <nav class="quick-nav">
- <h3>Quick Links</h3>
- <ul>
- <li><a href="/">Home</a></li>
- <li><a href="/articles/">Articles</a></li>
- <li><a href="/is/">About</a></li>
- <li><a href="/is/here">Contact</a></li>
- </ul>
- </nav>
- </div>
-
- <!-- Right: Recent Articles + Terminal -->
- <div class="hero-right-404">
- <div class="recent-articles">
- <h3>Recent Articles</h3>
- <ul>
- {{ range first 5 (.Site.RegularPages.ByDate.Reverse) }}
- <li>
- <a href="{{ .RelPermalink }}" class="article-link" data-type="{{ .Params.type }}">
- {{ .Title }}
- </a>
- </li>
- {{ end }}
- </ul>
- </div>
-
- <div class="terminal-widget">
- <div class="terminal-bar">
- <span class="terminal-dot" style="background: #ff6b6b;"></span>
- <span class="terminal-dot" style="background: #ffd93d;"></span>
- <span class="terminal-dot" style="background: #6bcf7f;"></span>
- </div>
- <div class="terminal-content">
- <div>$ <span class="terminal-prompt">curl https://danix.xyz/lost</span></div>
- <div>404: Not Found</div>
- <div>$ <span class="terminal-prompt">ls /articles</span></div>
- <div id="terminal-files"></div>
- </div>
- </div>
- </div>
- </div>
- </main>
-
- {{ partial "footer.html" . }}
-
- <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
- <script>feather.replace();</script>
-
- {{ $notFoundJS := resources.Get "js/404.js" | fingerprint }}
- <script src="{{ $notFoundJS.RelPermalink }}"></script>
-</body>
-</html>
diff --git a/layouts/404.it.html b/layouts/404.it.html
new file mode 100644
index 0000000..d876371
--- /dev/null
+++ b/layouts/404.it.html
@@ -0,0 +1,142 @@
+{{ define "main" }}
+
+<main class="min-h-screen px-4 py-12">
+ <div class="mx-auto px-4 py-12 max-w-4xl border border-border glow-accent rounded-lg bg-bg p-8" x-data="notFoundPage()">
+ <div class="text-center">
+ <!-- 404 Heading -->
+ <h1 class="text-7xl md:text-8xl font-bold text-accent mb-4 animate-fade-in">
+ 404
+ </h1>
+
+ <!-- Error Message -->
+ <h2 class="text-3xl md:text-4xl font-bold mb-6">
+ {{ i18n "notFound" }}
+ </h2>
+
+ <p class="text-lg text-text-dim mb-8">
+ {{ i18n "notFoundMessage" }}
+ </p>
+
+ <!-- Search Box -->
+ <div class="mb-12">
+ <form id="search-form" class="flex flex-col gap-4">
+ <label for="search-input" class="sr-only">{{ i18n "searchPlaceholder" }}</label>
+ <input
+ id="search-input"
+ type="text"
+ placeholder="{{ (i18n "searchPlaceholder") }}"
+ class="px-4 py-3 border-2 border-border rounded focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent bg-bg text-text"
+ @input="filterArticles($el.value)"
+ />
+ </form>
+ <div id="search-results" class="mt-4 text-left space-y-3" x-show="filteredArticles.length > 0">
+ <template x-for="article in filteredArticles" :key="article.title">
+ <div class="p-4 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors">
+ <a :href="article.url" class="block text-left">
+ <h4 class="font-bold text-accent hover:underline" x-text="article.title"></h4>
+ <p class="text-sm text-text-dim mt-1" x-text="article.date"></p>
+ </a>
+ </div>
+ </template>
+ </div>
+ <div x-show="searchQuery && filteredArticles.length === 0" class="mt-4 text-text-dim">
+ {{ i18n "noSearchResults" }}
+ </div>
+ </div>
+
+ <!-- Recent Articles Section -->
+ <div class="mb-12">
+ <h3 class="text-2xl font-bold mb-6">{{ i18n "recentArticles" }}</h3>
+ <div class="space-y-4">
+ {{ range first 5 (where .Site.RegularPages "Section" "articles") }}
+ <div class="p-4 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors">
+ <a href="{{ .Permalink }}" class="block text-left">
+ <h4 class="font-bold text-accent hover:underline">{{ .Title }}</h4>
+ <p class="text-sm text-text-dim mt-1">
+ {{ .Date.Format "Jan 02, 2006" }}
+ </p>
+ </a>
+ </div>
+ {{ end }}
+ </div>
+ </div>
+
+ <!-- Explore Topics -->
+ <div class="mb-12 text-left">
+ {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false "headingLevel" "h3") }}
+ </div>
+
+ <!-- Navigation Links -->
+ <div class="space-y-4 flex flex-col items-center mb-12">
+ <a href="/it/" class="btn btn-primary">
+ {{ i18n "goHome" }}
+ </a>
+ <a href="/it/articles/" class="btn btn-secondary">
+ {{ i18n "browseArticles" }}
+ </a>
+ <a href="/it/is/here/" class="btn btn-outline">
+ {{ i18n "contactSupport" }}
+ </a>
+ </div>
+
+ <!-- Easter Egg Trigger -->
+ <div class="mt-12 pt-8 border-t border-border">
+ <button
+ type="button"
+ @click="toggleEasterEgg()"
+ class="text-sm text-text-dim hover:text-accent transition-colors underline"
+ >
+ {{ i18n "followWhiteRabbit" }}
+ </button>
+ </div>
+
+ <!-- Easter Egg Modal (Hidden by default) -->
+ <div
+ class="fixed inset-0 z-50"
+ :class="{ 'flex items-center justify-center': showEasterEgg, 'hidden': !showEasterEgg }"
+ x-show="showEasterEgg"
+ x-cloak
+ >
+ <!-- Overlay -->
+ <div
+ class="absolute inset-0 bg-black/50"
+ @click="showEasterEgg = false"
+ ></div>
+
+ <!-- Modal Content -->
+ <div class="relative bg-bg border-2 border-accent p-8 rounded-lg shadow-xl max-w-md mx-4">
+ <h2 class="text-2xl font-bold mb-6 text-accent">{{ i18n "easterEggTitle" }}</h2>
+
+ <div class="space-y-4">
+ <button
+ type="button"
+ @click="showEasterEgg = false; window.location.href = '{{ .Site.BaseURL }}it/'"
+ class="w-full btn btn-primary"
+ >
+ 💊 {{ i18n "bluePill" }}
+ </button>
+
+ <button
+ type="button"
+ @click="goToRandomArticle()"
+ class="w-full btn btn-secondary"
+ >
+ 🐰 {{ i18n "redPill" }}
+ </button>
+ </div>
+
+ <button
+ type="button"
+ @click="showEasterEgg = false"
+ class="absolute top-4 right-4 text-text-dim hover:text-text dark:hover:text-text transition-colors"
+ aria-label="Close modal"
+ >
+ ✕
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</main>
+
+{{ end }}
diff --git a/layouts/_default/_markup/render-codeblock.html b/layouts/_default/_markup/render-codeblock.html
new file mode 100644
index 0000000..6f3e357
--- /dev/null
+++ b/layouts/_default/_markup/render-codeblock.html
@@ -0,0 +1,23 @@
+{{- $lang := .Type -}}
+{{- $hasLang := gt (len $lang) 0 -}}
+{{- $highlightLang := $lang -}}
+{{- if not $hasLang -}}{{- $highlightLang = "text" -}}{{- end -}}
+{{- $opts := dict "lineNos" true "lineNumbersInTable" true -}}
+
+<div class="code-block-wrapper not-prose">
+ {{- if $hasLang -}}
+ <div class="code-header" data-lang="{{ $lang }}">
+ <span class="code-lang-label">{{ $lang }}</span>
+ <div class="code-copy-wrapper">
+ <span role="status" aria-live="polite" class="sr-only code-copy-status"></span>
+ <button class="code-copy-btn" aria-label="Copy code" data-copy-target>
+ <i data-feather="copy" class="icon-copy" aria-hidden="true"></i>
+ <i data-feather="check" class="icon-check hidden" aria-hidden="true"></i>
+ </button>
+ </div>
+ </div>
+ {{- end -}}
+ <div class="code-body">
+{{- highlight .Inner $highlightLang $opts -}}
+ </div>
+</div>
diff --git a/layouts/_default/_markup/render-heading.html b/layouts/_default/_markup/render-heading.html
new file mode 100644
index 0000000..d0d5e05
--- /dev/null
+++ b/layouts/_default/_markup/render-heading.html
@@ -0,0 +1,5 @@
+{{- if eq .Level 1 -}}
+<h1 id="{{ .Anchor }}">{{ .Text | safeHTML }}</h1>
+{{- else -}}
+<h{{ .Level }} id="{{ .Anchor }}"><span aria-hidden="true" class="heading-prefix">//</span> {{ .Text | safeHTML }}</h{{ .Level }}>
+{{- end -}}
diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html
new file mode 100644
index 0000000..482006e
--- /dev/null
+++ b/layouts/_default/baseof.html
@@ -0,0 +1,130 @@
+<!DOCTYPE html>
+<html lang="{{ .Lang }}" class="theme-dark">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ {{ partial "head-meta.html" . }}
+ <title>{{ .Title }}{{ if ne .Title .Site.Title }} — {{ .Site.Title }}{{ end }}</title>
+
+ <!-- Favicon -->
+ <link rel="icon" type="image/png" href="/images/fav.png">
+
+ <!-- Fonts -->
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&family=Oxanium:wght@400;600;700&display=swap" rel="stylesheet">
+
+ <!-- Feather Icons -->
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.css">
+
+ <!-- Tailwind CSS -->
+ {{ $css := resources.Get "css/main.min.css" }}
+ <link rel="stylesheet" href="{{ $css.RelPermalink }}">
+
+ <!-- Syntax highlighting (Chroma) -->
+ {{ $chroma := resources.Get "css/chroma-custom.css" | minify }}
+ <link rel="stylesheet" href="{{ $chroma.RelPermalink }}">
+</head>
+<body class="bg-bg text-text antialiased" data-page-kind="{{ if .IsHome }}home{{ else }}other{{ end }}">
+ <!-- Reading progress bar (only on single pages/articles) -->
+ {{ if eq .Kind "page" }}
+ <div
+ id="reading-progress"
+ class="fixed top-0 left-0 h-1 transition-all duration-100"
+ style="width: 0%; background: linear-gradient(to right, var(--accent), var(--accent2)); z-index: 9999;"
+ ></div>
+ {{ partial "back-to-top.html" . }}
+ {{ end }}
+ <!-- Skip to main content link -->
+ <a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-accent focus:text-white focus:rounded">
+ {{ i18n "skipToContent" }}
+ </a>
+
+ <!-- Dot grid background pattern -->
+ <div class="fixed inset-0 pointer-events-none opacity-5 dot-grid" style="
+ background-image: radial-gradient(circle, currentColor 1px, transparent 1px);
+ background-size: 30px 30px;
+ z-index: -1;
+ "></div>
+
+ <!-- Matrix rain canvas background -->
+ <canvas id="matrix-rain" aria-hidden="true"></canvas>
+
+ <!-- Theme toggle & language toggle (before Alpine loads to prevent flash) -->
+ <script>
+ (function() {
+ const theme = localStorage.getItem('theme') || 'dark';
+ const html = document.documentElement;
+ html.classList.remove('theme-light', 'theme-dark');
+ html.classList.add('theme-' + theme);
+ })();
+ </script>
+
+ <!-- Navigation -->
+ {{ partial "header.html" . }}
+
+ <!-- Main content (spaced for fixed header) -->
+ <main id="main" class="mt-20 relative z-10">
+ {{ block "main" . }}{{ end }}
+ </main>
+
+ <!-- Footer -->
+ {{ partial "footer.html" . }}
+
+ <!-- Search modal (desktop and mobile) -->
+ {{ partial "search-modal.html" . }}
+
+ <!-- Alpine.js -->
+ <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
+
+ <!-- Feather Icons initialization -->
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
+ <script>feather.replace();</script>
+
+ <!-- Theme toggle script -->
+ {{ $themeScript := resources.Get "js/theme-toggle.js" | minify }}
+ <script src="{{ $themeScript.RelPermalink }}"></script>
+
+ <!-- Menu script -->
+ {{ $menuScript := resources.Get "js/menu.js" | minify }}
+ <script src="{{ $menuScript.RelPermalink }}"></script>
+
+ <!-- Contact form script -->
+ {{ $contactScript := resources.Get "js/contact-form.js" | minify }}
+ <script src="{{ $contactScript.RelPermalink }}"></script>
+
+ <!-- 404 Not Found page script -->
+ {{ $notFoundScript := resources.Get "js/not-found-page.js" | minify }}
+ <script src="{{ $notFoundScript.RelPermalink }}"></script>
+
+ <!-- Reading progress bar script (on single pages/articles) -->
+ {{ if eq .Kind "page" }}
+ {{ $progressScript := resources.Get "js/reading-progress.js" | minify }}
+ <script src="{{ $progressScript.RelPermalink }}"></script>
+ {{ end }}
+
+ <!-- Code block copy button -->
+ {{ if eq .Kind "page" }}
+ {{ $codeScript := resources.Get "js/code-copy.js" | minify }}
+ <script src="{{ $codeScript.RelPermalink }}"></script>
+ {{ end }}
+
+ <!-- Matrix rain background effect -->
+ {{ with resources.Get "js/matrix-rain.js" }}
+ {{ $s := . | minify }}
+ <script src="{{ $s.RelPermalink }}"></script>
+ {{ end }}
+
+ <!-- Search functionality script -->
+ {{ $searchScript := resources.Get "js/search.js" | minify }}
+ <script src="{{ $searchScript.RelPermalink }}"></script>
+
+ <!-- Timeline lazy-reveal (scroll-triggered reveal animation) -->
+ {{ if eq .Kind "section" }}
+ {{ $lazyScript := resources.Get "js/article-lazy.js" | minify }}
+ <script src="{{ $lazyScript.RelPermalink }}"></script>
+ {{ end }}
+
+ <!-- Tag cloud spiral layout -->
+ {{ $tagCloudScript := resources.Get "js/tag-cloud-spiral.js" | minify }}
+ <script src="{{ $tagCloudScript.RelPermalink }}"></script>
+</body>
+</html>
diff --git a/layouts/_default/list.html b/layouts/_default/list.html
new file mode 100644
index 0000000..09c0cbb
--- /dev/null
+++ b/layouts/_default/list.html
@@ -0,0 +1,39 @@
+{{ define "main" }}
+<div class="mx-auto px-4 py-12 max-w-5xl">
+ <h1 class="text-4xl md:text-5xl font-bold text-accent mb-12">
+ {{ .Title }}
+ </h1>
+
+ {{ $pinned := where .Pages "Params.pinned" true }}
+ {{ $unpinned := where .Pages "Params.pinned" false }}
+ {{ if eq (len $unpinned) 0 }}
+ {{ $unpinned = where .Pages "Params.pinned" nil }}
+ {{ if eq (len $unpinned) 0 }}
+ {{ $unpinned = where .Pages "Params.pinned" "" }}
+ {{ end }}
+ {{ end }}
+
+ {{ $allPages := ($pinned | append $unpinned) }}
+
+ {{ if eq (len $allPages) 0 }}
+ <div class="py-12 text-center text-text-dim">
+ {{ i18n "noRelated" }}
+ </div>
+ {{ else }}
+ <ol class="timeline" aria-label="{{ .Title }}">
+ {{ range $i, $page := (sort $pinned "Date" "desc") }}
+ {{ $side := "left" }}
+ {{ if eq (mod $i 2) 0 }}{{ $side = "left" }}{{ else }}{{ $side = "right" }}{{ end }}
+ {{ partial "article-list-item.html" (dict "Page" $page "side" $side) }}
+ {{ end }}
+ {{ $offset := len $pinned }}
+ {{ range $i, $page := (sort $unpinned "Date" "desc") }}
+ {{ $idx := add $i $offset }}
+ {{ $side := "left" }}
+ {{ if eq (mod $idx 2) 0 }}{{ $side = "left" }}{{ else }}{{ $side = "right" }}{{ end }}
+ {{ partial "article-list-item.html" (dict "Page" $page "side" $side) }}
+ {{ end }}
+ </ol>
+ {{ end }}
+</div>
+{{ end }}
diff --git a/layouts/_default/single.html b/layouts/_default/single.html
new file mode 100644
index 0000000..7a1a069
--- /dev/null
+++ b/layouts/_default/single.html
@@ -0,0 +1,54 @@
+{{ define "main" }}
+<article class="mx-auto px-4 py-12 max-w-7xl">
+ <div class="grid md:grid-cols-3 gap-8 content-grid">
+ <!-- Article section -->
+ <div class="md:col-span-2 min-w-0">
+ <!-- Top article navigation (articles only) -->
+ {{ if eq .Section "articles" }}
+ {{ partial "article-nav.html" (dict "page" . "variant" "top") }}
+ {{ end }}
+
+ <!-- Breadcrumb -->
+ {{ partial "breadcrumb.html" . }}
+
+ <!-- Article header -->
+ {{ partial "article-header.html" . }}
+
+ <!-- Article content -->
+ <div class="prose prose-invert max-w-none mb-12">
+ {{ .Content }}
+ </div>
+
+ <!-- Tags section -->
+ {{ if .Params.tags }}
+ <div class="border-t border-border pt-8">
+ <h3 class="text-lg font-semibold text-accent mb-4">{{ i18n "tags" }}</h3>
+ <div class="flex flex-wrap gap-2">
+ {{ $lang := .Lang }}
+ {{ range .Params.tags }}
+ {{ $tagUrl := printf "/tags/%s/" (. | urlize) }}
+ {{ if eq $lang "it" }}
+ {{ $tagUrl = printf "/it/tags/%s/" (. | urlize) }}
+ {{ end }}
+ <a
+ href="{{ $tagUrl }}"
+ class="inline-flex items-center px-3 py-1 border border-border/30 rounded hover:border-accent/50 hover:text-accent transition-colors text-sm"
+ >
+ {{ . }}
+ </a>
+ {{ end }}
+ </div>
+ </div>
+ {{ end }}
+
+ <!-- Bottom article navigation (articles only) -->
+ {{ if eq .Section "articles" }}
+ {{ partial "article-nav.html" (dict "page" . "variant" "bottom") }}
+ {{ end }}
+ </div>
+
+ <!-- Sidebar -->
+ {{ partial "sidebar.html" . }}
+ </div>
+</article>
+{{ end }}
diff --git a/layouts/_partials/article-single.html b/layouts/_partials/article-single.html
deleted file mode 100644
index f030869..0000000
--- a/layouts/_partials/article-single.html
+++ /dev/null
@@ -1,56 +0,0 @@
-<article class="article-single">
- <!-- Article Hero -->
- {{ if .Params.image }}
- <div class="article-hero" style="background-image: url('{{ .Params.image }}')">
- <div class="article-hero-overlay"></div>
- <div class="article-hero-content">
- <nav class="article-breadcrumb">
- <a href="/articles/">Articles</a>
- <span>/</span>
- <span>{{ .Title }}</span>
- </nav>
- <h1>{{ .Title }}</h1>
- </div>
- </div>
- {{ end }}
-
- <!-- Article Meta Bar -->
- <div class="article-meta-bar">
- <div class="article-meta">
- <span class="article-type-badge type-{{ .Params.type }}">{{ .Params.type }}</span>
- <span class="article-date">{{ .Date.Format "Jan 02, 2006" }}</span>
- <span class="article-read-time">{{ .ReadingTime }} min read</span>
- </div>
- </div>
-
- <!-- Share Sidebar (for large screens) -->
- {{ partial "share-sidebar.html" . }}
-
- <!-- Article Body -->
- <div class="article-body container-narrow">
- {{ .Content }}
- </div>
-
- <!-- Article Footer Nav -->
- {{ $section := .Site.GetPage .Section }}
- {{ if $section }}
- {{ $prevPage := .PrevInSection }}
- {{ $nextPage := .NextInSection }}
- {{ if or $prevPage $nextPage }}
- <nav class="article-footer-nav">
- {{ if $prevPage }}
- <a href="{{ $prevPage.RelPermalink }}" class="nav-prev">
- <span class="nav-label">← Previous</span>
- <span class="nav-title">{{ $prevPage.Title }}</span>
- </a>
- {{ end }}
- {{ if $nextPage }}
- <a href="{{ $nextPage.RelPermalink }}" class="nav-next">
- <span class="nav-label">Next →</span>
- <span class="nav-title">{{ $nextPage.Title }}</span>
- </a>
- {{ end }}
- </nav>
- {{ end }}
- {{ end }}
-</article>
diff --git a/layouts/_partials/featured-card.html b/layouts/_partials/featured-card.html
deleted file mode 100644
index cf2618f..0000000
--- a/layouts/_partials/featured-card.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{{ $page := .page }}
-
-<article class="featured-article">
- {{ if $page.Params.image }}
- <div class="featured-image">
- <img src="{{ $page.Params.image }}" alt="{{ $page.Title }}" loading="lazy">
- </div>
- {{ end }}
-
- <div class="featured-body">
- <div class="featured-header">
- <span class="featured-type-badge" style="color: var(--type-{{ $page.Params.type }});">
- {{ $page.Params.type }}
- </span>
- <span class="featured-date">$ {{ $page.Date.Format "2006-01-02" }}</span>
- </div>
- <h2 class="featured-title">{{ $page.Title }}</h2>
- {{ with $page.Params.description }}
- <p class="featured-excerpt">{{ . }}</p>
- {{ end }}
- <a href="{{ $page.RelPermalink }}" class="featured-link">Read the full article →</a>
- </div>
-</article>
diff --git a/layouts/_partials/footer.html b/layouts/_partials/footer.html
deleted file mode 100644
index a9df047..0000000
--- a/layouts/_partials/footer.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="footer-container">
- <div class="footer-content">
- <div class="footer-copyright">
- © {{ now.Year }} {{ .Site.Params.author }}. All rights reserved.
- </div>
- <nav>
- <ul class="footer-nav">
- {{ range .Site.Menus.main }}
- <li><a href="{{ .URL }}">{{ .Name }}</a></li>
- {{ end }}
- </ul>
- </nav>
- </div>
-</div>
diff --git a/layouts/_partials/head.html b/layouts/_partials/head.html
deleted file mode 100644
index 1a8a727..0000000
--- a/layouts/_partials/head.html
+++ /dev/null
@@ -1,8 +0,0 @@
-<meta charset="utf-8">
-<meta name="viewport" content="width=device-width, initial-scale=1">
-<meta name="description" content="{{ .Site.Params.description }}">
-<meta name="theme-color" content="#060b10">
-<title>{{ if .IsHome }}{{ .Site.Title }}{{ else }}{{ .Title }} | {{ .Site.Title }}{{ end }}</title>
-
-{{ partialCached "head/css.html" . }}
-{{ partialCached "head/js.html" . }}
diff --git a/layouts/_partials/head/css.html b/layouts/_partials/head/css.html
deleted file mode 100644
index 8897866..0000000
--- a/layouts/_partials/head/css.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{{- with resources.Get "css/main.css" }}
- {{- $opts := dict
- "minify" (cond hugo.IsDevelopment false true)
- "sourceMap" (cond hugo.IsDevelopment "linked" "none")
- }}
- {{- with . | css.Build $opts }}
- {{- if hugo.IsDevelopment }}
- <link rel="stylesheet" href="{{ .RelPermalink }}">
- {{- else }}
- {{- with . | fingerprint }}
- <link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end }}
diff --git a/layouts/_partials/head/js.html b/layouts/_partials/head/js.html
deleted file mode 100644
index 0210efa..0000000
--- a/layouts/_partials/head/js.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{{- with resources.Get "js/main.js" }}
- {{- $opts := dict
- "minify" (cond hugo.IsDevelopment false true)
- "sourceMap" (cond hugo.IsDevelopment "linked" "none")
- }}
- {{- with . | js.Build $opts }}
- {{- if hugo.IsDevelopment }}
- <script src="{{ .RelPermalink }}"></script>
- {{- else }}
- {{- with . | fingerprint }}
- <script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
- {{- end }}
- {{- end }}
- {{- end }}
-{{- end }}
diff --git a/layouts/_partials/header.html b/layouts/_partials/header.html
deleted file mode 100644
index 1f8efb8..0000000
--- a/layouts/_partials/header.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<nav class="nav-header" role="navigation" aria-label="Main navigation">
- <button class="hamburger" id="hamburger-btn" aria-label="Toggle menu" aria-expanded="false">
- <span></span>
- <span></span>
- <span></span>
- </button>
-
- <div class="menu-overlay" id="menu-overlay">
- <div class="menu-items">
- <a href="/" class="menu-logo">{{ .Site.Params.author }}</a>
-
- <ul class="menu-links">
- {{ range .Site.Menus.main }}
- <li>
- <a href="{{ .URL }}" {{ if (in $.RelPermalink .URL) }}aria-current="page"{{ end }}>
- {{ .Name }}
- </a>
- </li>
- {{ end }}
- </ul>
-
- <div class="menu-footer">
- <button class="theme-switch" id="theme-switch" aria-label="Toggle theme">
- <span class="theme-switch-dot"></span>
- </button>
- </div>
- </div>
- </div>
-</nav>
diff --git a/layouts/_partials/hero.html b/layouts/_partials/hero.html
deleted file mode 100644
index 57d223d..0000000
--- a/layouts/_partials/hero.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<section class="hero" role="region" aria-label="Hero">
- <canvas id="matrix-canvas" data-mode="hero" aria-hidden="true"></canvas>
- <div class="hero-container">
- <div class="hero-left">
- <div class="hero-prompt">welcome to</div>
- <h1 class="hero-name" data-text="{{ .Site.Params.author }}">{{ .Site.Params.author }}</h1>
- <div class="hero-role" id="typed" data-phrases='{{ .Site.Params.typingPhrases | jsonify }}'></div>
- <p class="hero-tagline">{{ .Site.Params.description }}</p>
- <div class="hero-buttons">
- <a href="/articles/" class="btn btn-primary">Read Articles</a>
- <a href="/is/" class="btn btn-outline">About Me</a>
- </div>
- </div>
- <div class="hero-right">
- <div class="hero-terminal">
- <div class="terminal-bar">
- <span class="terminal-dot" style="background: #ff5f57;"></span>
- <span class="terminal-dot" style="background: #febc2e;"></span>
- <span class="terminal-dot" style="background: #28c840;"></span>
- <span class="terminal-title">root@danix.xyz</span>
- </div>
- <div class="terminal-content">
- <div class="tl tl-d1"><span class="tc-dim">$ </span><span class="tc-ok">whoami</span></div>
- <div class="tl tl-d2">danilo</div>
- <div class="tl tl-d3"><span class="tc-dim">$ </span><span class="tc-ok">cat roles.txt</span></div>
- <div class="tl tl-d4"><span class="tc-key">Security</span> &amp; Web Dev</div>
- <div class="tl tl-d5"><span class="tc-key">WordPress</span> Developer</div>
- <div class="tl tl-d6"><span class="tc-key">Bash</span> Enthusiast</div>
- <div class="tl tl-d7"><span class="tc-dim">$ </span><span class="tc-ok">uptime</span></div>
- <div class="tl tl-d8">up 4 years, still learning</div>
- <div class="tl tl-d9"><span class="tc-dim">$ </span><span class="tc-ok">_</span></div>
- </div>
- </div>
- </div>
- </div><!-- /.hero-container -->
-
- <div class="scroll-indicator" aria-hidden="true">
- <span>scroll</span>
- <div class="scroll-line"></div>
- </div>
-</section>
diff --git a/layouts/_partials/menu.html b/layouts/_partials/menu.html
deleted file mode 100644
index 14245b5..0000000
--- a/layouts/_partials/menu.html
+++ /dev/null
@@ -1,51 +0,0 @@
-{{- /*
-Renders a menu for the given menu ID.
-
-@context {page} page The current page.
-@context {string} menuID The menu ID.
-
-@example: {{ partial "menu.html" (dict "menuID" "main" "page" .) }}
-*/}}
-
-{{- $page := .page }}
-{{- $menuID := .menuID }}
-
-{{- with index site.Menus $menuID }}
- <nav>
- <ul>
- {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }}
- </ul>
- </nav>
-{{- end }}
-
-{{- define "_partials/inline/menu/walk.html" }}
- {{- $page := .page }}
- {{- range .menuEntries }}
- {{- $attrs := dict "href" .URL }}
- {{- if $page.IsMenuCurrent .Menu . }}
- {{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }}
- {{- else if $page.HasMenuCurrent .Menu .}}
- {{- $attrs = merge $attrs (dict "class" "ancestor" "aria-current" "true") }}
- {{- end }}
- {{- $name := .Name }}
- {{- with .Identifier }}
- {{- with T . }}
- {{- $name = . }}
- {{- end }}
- {{- end }}
- <li>
- <a
- {{- range $k, $v := $attrs }}
- {{- with $v }}
- {{- printf " %s=%q" $k $v | safeHTMLAttr }}
- {{- end }}
- {{- end -}}
- >{{ $name }}</a>
- {{- with .Children }}
- <ul>
- {{- partial "inline/menu/walk.html" (dict "page" $page "menuEntries" .) }}
- </ul>
- {{- end }}
- </li>
- {{- end }}
-{{- end }}
diff --git a/layouts/_partials/photo-article.html b/layouts/_partials/photo-article.html
deleted file mode 100644
index 1040c9b..0000000
--- a/layouts/_partials/photo-article.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<article class="article-single">
- <!-- Photo Hero -->
- {{ if .Params.image }}
- <div class="article-hero" style="background-image: url('{{ .Params.image }}')">
- <div class="article-hero-overlay"></div>
- <div class="article-hero-content">
- <h1>{{ .Title }}</h1>
- </div>
- </div>
- {{ end }}
-
- <!-- Article Meta Bar -->
- <div class="article-meta-bar">
- <div class="article-meta">
- <span class="article-type-badge type-{{ .Params.type }}">{{ .Params.type }}</span>
- <span class="article-date">{{ .Date.Format "Jan 02, 2006" }}</span>
- </div>
- </div>
-
- <!-- Photo Grid -->
- <div class="article-body container-narrow">
- <div class="photo-grid" data-lightbox="true">
- {{ .Content }}
- </div>
- </div>
-
- <!-- Load photo utilities -->
- {{ $photoUtils := resources.Get "js/photo-utils.js" }}
- {{ $lightbox := resources.Get "js/lightbox.js" | fingerprint }}
- <script src="{{ $photoUtils.RelPermalink }}"></script>
- <script src="{{ $lightbox.RelPermalink }}"></script>
-</article>
diff --git a/layouts/_partials/post-card.html b/layouts/_partials/post-card.html
deleted file mode 100644
index f3a1362..0000000
--- a/layouts/_partials/post-card.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{{ if eq .context "home" }}
- <article class="article-card reveal" data-type="{{ .type }}">
- <div class="article-type" style="background: var(--type-{{ .type }});">{{ .type }}</div>
- <div class="article-content">
- <h3 class="article-title"><a href="{{ .url }}">{{ .title }}</a></h3>
- <p class="article-excerpt">{{ .description }}</p>
- <div class="article-meta">
- <span>{{ dateFormat "Jan 2, 2006" .date }}</span>
- <a href="{{ .url }}" class="article-read">Read →</a>
- </div>
- </div>
- </article>
-{{ else }}
- {{ $featured := .featured }}
- <article class="post-card {{ if $featured }}featured{{ end }}">
- {{ if .image }}
- <img src="{{ .image }}" alt="{{ .title }}" class="post-card-image">
- {{ else }}
- <div class="post-card-image" style="background: linear-gradient(135deg, var(--color-{{ .type }}), var(--bg2));"></div>
- {{ end }}
- <div class="post-card-body">
- <div class="post-type-badge {{ .type }}">{{ .type }}</div>
- <h3 class="post-card-title"><a href="{{ .url }}">{{ .title }}</a></h3>
- <p class="post-card-excerpt">{{ .description }}</p>
- <div class="post-card-meta">
- <span>{{ dateFormat "Jan 2, 2006" .date }}</span>
- </div>
- </div>
- </article>
-{{ end }}
diff --git a/layouts/_partials/share-sidebar.html b/layouts/_partials/share-sidebar.html
deleted file mode 100644
index 36b6d03..0000000
--- a/layouts/_partials/share-sidebar.html
+++ /dev/null
@@ -1,28 +0,0 @@
-<aside class="share-sidebar" id="share-sidebar" data-url="{{ .Permalink }}" data-title="{{ .Title | htmlEscape }}">
- <div class="share-buttons">
- <button class="share-btn" data-platform="whatsapp" title="Share on WhatsApp">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path></svg>
- </button>
- <button class="share-btn" data-platform="telegram" title="Share on Telegram">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>
- </button>
- <button class="share-btn" data-platform="linkedin" title="Share on LinkedIn">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6zM2 9h4v12H2z"></path><circle cx="4" cy="4" r="2"></circle></svg>
- </button>
- <button class="share-btn" data-platform="twitter" title="Share on X (Twitter)">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2s9 5 20 0a9.5 9.5 0 0 0 5-6.5"></path></svg>
- </button>
- <button class="share-btn" data-platform="facebook" title="Share on Facebook">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 2h-3a6 6 0 0 0-6 6v4a6 6 0 0 0 6 6h3"></path><path d="M16 18v2"></path><path d="M6 6v12"></path></svg>
- </button>
- <button class="share-btn" data-platform="reddit" title="Share on Reddit">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"></circle><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm7-3c0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3 3 1.34 3 3z"></path></svg>
- </button>
- <button class="share-btn" data-platform="email" title="Share via Email">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"></rect><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"></path></svg>
- </button>
- <button class="share-btn" id="share-copy" title="Copy link">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
- </button>
- </div>
-</aside>
diff --git a/layouts/_partials/static-page.html b/layouts/_partials/static-page.html
deleted file mode 100644
index d557b2f..0000000
--- a/layouts/_partials/static-page.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<article class="static-page">
- <!-- Static Page Hero -->
- <div class="page-hero">
- <div class="page-hero-overlay"></div>
- <div class="page-hero-content">
- <h1>{{ .Title }}</h1>
- </div>
- </div>
-
- <!-- Page Content -->
- <div class="page-content container-narrow">
- {{ .Content }}
- </div>
-
- <!-- Related Pages Nav (for is/* section) -->
- {{ if eq .Section "is" }}
- <nav class="page-nav" style="margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--border);">
- <h3 style="margin-bottom: 1rem;">Other pages</h3>
- <ul style="list-style: none; margin: 0;">
- {{ $section := .Site.GetPage "/is" }}
- {{ if $section }}
- {{ range $section.Pages }}
- <li style="margin-bottom: 0.5rem;">
- <a href="{{ .RelPermalink }}" {{ if eq $.RelPermalink .RelPermalink }}class="active"{{ end }}>
- {{ .Title }}
- </a>
- </li>
- {{ end }}
- {{ end }}
- </ul>
- </nav>
- {{ end }}
-</article>
diff --git a/layouts/_partials/terms.html b/layouts/_partials/terms.html
deleted file mode 100644
index 8a6ebec..0000000
--- a/layouts/_partials/terms.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{{- /*
-For a given taxonomy, renders a list of terms assigned to the page.
-
-@context {page} page The current page.
-@context {string} taxonomy The taxonomy.
-
-@example: {{ partial "terms.html" (dict "taxonomy" "tags" "page" .) }}
-*/}}
-
-{{- $page := .page }}
-{{- $taxonomy := .taxonomy }}
-
-{{- with $page.GetTerms $taxonomy }}
- {{- $label := (index . 0).Parent.LinkTitle }}
- <div>
- <div>{{ $label }}:</div>
- <ul>
- {{- range . }}
- <li><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></li>
- {{- end }}
- </ul>
- </div>
-{{- end }}
diff --git a/layouts/_partials/timeline-item.html b/layouts/_partials/timeline-item.html
deleted file mode 100644
index fcb3f50..0000000
--- a/layouts/_partials/timeline-item.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{{ $page := .page }}
-{{ $index := .index }}
-{{ $position := "left" }}
-{{ if (eq (mod $index 2) 1) }}
- {{ $position = "right" }}
-{{ end }}
-
-<article class="timeline-item {{ $position }}" data-type="{{ $page.Params.type }}">
- <div class="timeline-dot"></div>
-
- <div class="article-card">
- {{ if $page.Params.image }}
- <div class="article-card-image">
- <img src="{{ $page.Params.image }}" alt="{{ $page.Title }}" loading="lazy">
- </div>
- {{ end }}
-
- <div class="article-card-body">
- <div class="article-card-header">
- <span class="article-type-badge type-{{ $page.Params.type }}">{{ $page.Params.type }}</span>
- <span class="article-date">{{ $page.Date.Format "Jan 02, 2006" }}</span>
- </div>
- <h3 class="article-card-title">
- <a href="{{ $page.RelPermalink }}">{{ $page.Title }}</a>
- </h3>
- {{ with $page.Params.description }}
- <p class="article-card-excerpt">{{ . }}</p>
- {{ end }}
- </div>
- </div>
-</article>
diff --git a/layouts/articles/single.html b/layouts/articles/single.html
new file mode 100644
index 0000000..e646639
--- /dev/null
+++ b/layouts/articles/single.html
@@ -0,0 +1,50 @@
+{{ define "main" }}
+{{ $articleType := .Params.type | default "life" }}
+{{ $template := printf "article-types/%s.html" $articleType }}
+<article class="mx-auto px-4 py-12">
+ <div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto content-grid">
+ <!-- Article section -->
+ <div class="md:col-span-2">
+ <!-- Top article navigation -->
+ {{ partial "article-nav.html" (dict "page" . "variant" "top") }}
+
+ <!-- Breadcrumb -->
+ {{ partial "breadcrumb.html" . }}
+
+ <!-- Article header -->
+ {{ partial "article-header.html" . }}
+
+ <!-- Type-specific content -->
+ {{ partial $template . }}
+
+ <!-- Tags section -->
+ {{ if .Params.tags }}
+ <div class="border-t border-border pt-8">
+ <h3 class="text-lg font-semibold text-accent mb-4">{{ i18n "tags" }}</h3>
+ <div class="flex flex-wrap gap-2">
+ {{ $lang := .Lang }}
+ {{ range .Params.tags }}
+ {{ $tagUrl := printf "/tags/%s/" (. | urlize) }}
+ {{ if eq $lang "it" }}
+ {{ $tagUrl = printf "/it/tags/%s/" (. | urlize) }}
+ {{ end }}
+ <a
+ href="{{ $tagUrl }}"
+ class="inline-flex items-center px-3 py-1 border border-border/30 rounded hover:border-accent/50 hover:text-accent transition-colors text-sm"
+ >
+ {{ . }}
+ </a>
+ {{ end }}
+ </div>
+ </div>
+ {{ end }}
+
+ <!-- Bottom article navigation -->
+ {{ partial "article-nav.html" (dict "page" . "variant" "bottom") }}
+ </div>
+
+ <!-- Sidebar -->
+ {{ partial "sidebar.html" . }}
+ </div>
+</article>
+{{ end }}
diff --git a/layouts/baseof.html b/layouts/baseof.html
deleted file mode 100644
index cdbb1e3..0000000
--- a/layouts/baseof.html
+++ /dev/null
@@ -1,23 +0,0 @@
-<!DOCTYPE html>
-<html lang="{{ site.Language.Locale }}" dir="{{ or site.Language.Direction `ltr` }}">
-<head>
- {{ partial "head.html" . }}
- <script>
- (function() {
- const theme = localStorage.getItem('theme') || 'dark';
- if (theme === 'light') {
- document.documentElement.classList.add('theme-light');
- }
- })();
- </script>
-</head>
-<body>
- <a href="#main-content" class="skip-link">Skip to content</a>
- {{ if and (not .IsHome) (ne .Kind "term") (ne .Kind "taxonomy") (ne .Kind "section") }}
- <div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
- {{ end }}
- {{ partial "header.html" . }}
- <main id="main-content">{{ block "main" . }}{{ end }}</main>
- <footer>{{ partial "footer.html" . }}</footer>
-</body>
-</html>
diff --git a/layouts/home.html b/layouts/home.html
deleted file mode 100644
index 54d8087..0000000
--- a/layouts/home.html
+++ /dev/null
@@ -1,48 +0,0 @@
-{{ define "canvas-mode" }}hero{{ end }}
-
-{{ define "main" }}
- {{ partial "hero.html" . }}
-
- <section id="articles" class="section-container reveal-group"
- style="background: var(--bg2); padding: 6rem 2rem;">
-
- <div class="section-header">
- <p class="section-eyebrow">// latest posts</p>
- <h2 class="section-title">Recent Articles</h2>
- </div>
-
- {{ $articlesSection := .Site.GetPage "/articles" }}
- {{ if $articlesSection }}
- {{ $articles := $articlesSection.Pages }}
- {{ $articles = sort $articles "Date" "desc" }}
- <div class="articles-grid">
- {{ range first 6 $articles }}
- {{ $type := .Params.type }}
- {{ if not $type }}{{ $type = "article" }}{{ end }}
-
- {{ $excerpt := .Params.excerpt }}
- {{ if not $excerpt }}
- {{ $excerpt = .Summary | plainify | truncate 150 }}
- {{ end }}
-
- {{ $data := dict
- "title" .Title
- "type" $type
- "description" $excerpt
- "date" .Date
- "url" .RelPermalink
- "image" .Params.image
- "featured" .Params.featured
- "context" "home"
- }}
- {{ partial "post-card.html" $data }}
- {{ end }}
- </div>
- {{ end }}
-
- <div style="text-align: center; margin-top: 3rem;">
- <a href="/articles/" class="btn btn-outline">View All Articles</a>
- </div>
-
- </section>
-{{ end }}
diff --git a/layouts/index.html b/layouts/index.html
new file mode 100644
index 0000000..fd3cfa0
--- /dev/null
+++ b/layouts/index.html
@@ -0,0 +1,59 @@
+{{ define "main" }}
+<section class="min-h-[calc(100vh-200px)] flex items-center justify-center px-4 py-12">
+ <div class="max-w-4xl w-full">
+ <!-- Profile Image (optional) -->
+ {{ if .Params.image }}
+ <div class="flex justify-center mb-8">
+ <img
+ src="{{ .Params.image }}"
+ alt="{{ .Site.Params.author }}"
+ loading="lazy"
+ class="w-32 h-32 md:w-48 md:h-48 rounded-full border-4 border-accent object-cover"
+ >
+ </div>
+ {{ end }}
+
+ <!-- Author Name -->
+<!-- <h1 class="text-4xl md:text-5xl font-bold text-accent text-center mb-4">
+ {{ .Site.Params.author }}
+ </h1>
+ -->
+ <!-- Bio (from page content) -->
+ <div class="text-xl text-text-dim text-center mb-8 leading-relaxed prose prose-invert max-w-none">
+ {{ .Content }}
+ </div>
+
+ <!-- Call-to-Action Buttons -->
+ <div class="flex flex-col sm:flex-row gap-4 justify-center">
+ {{ $lang := .Lang }}
+ {{ $articlesUrl := "/articles/" }}
+ {{ $contactUrl := "/is/here/" }}
+ {{ if eq $lang "it" }}
+ {{ $articlesUrl = "/it/articles/" }}
+ {{ $contactUrl = "/it/is/here/" }}
+ {{ end }}
+
+ <!-- Articles Button (Primary) -->
+ <a
+ href="{{ $articlesUrl }}"
+ class="btn btn-primary btn-lg"
+ >
+ {{ i18n "articles" }}
+ </a>
+
+ <!-- Contact Button (Outline) -->
+ <a
+ href="{{ $contactUrl }}"
+ class="btn btn-outline btn-lg"
+ >
+ {{ i18n "contact" }}
+ </a>
+ </div>
+
+ <!-- Tag Cloud Section -->
+ <div class="mt-16 pt-8 border-t border-border">
+ {{ partial "tag-cloud.html" (dict "page" . "showCount" true "wrapInWidget" false) }}
+ </div>
+ </div>
+</section>
+{{ end }}
diff --git a/layouts/index.json b/layouts/index.json
new file mode 100644
index 0000000..e17e31e
--- /dev/null
+++ b/layouts/index.json
@@ -0,0 +1,13 @@
+{{- $articles := where .Site.RegularPages "Section" "articles" -}}
+{{- $articles = $articles.ByDate.Reverse -}}
+[
+ {{- range $index, $article := $articles -}}
+ {
+ "title": {{ $article.Title | jsonify }},
+ "url": {{ $article.Permalink | jsonify }},
+ "date": {{ $article.Date.Format "Jan 02, 2006" | jsonify }},
+ "summary": {{ substr ($article.Summary | plainify) 0 160 | jsonify }}
+ }
+ {{- if ne (add $index 1) (len $articles) }},{{ end }}
+ {{- end }}
+]
diff --git a/layouts/is/list.html b/layouts/is/list.html
new file mode 100644
index 0000000..128414f
--- /dev/null
+++ b/layouts/is/list.html
@@ -0,0 +1,22 @@
+{{ define "main" }}
+<div class="mx-auto px-4 py-12">
+ <div class="grid md:grid-cols-3 gap-8 max-w-7xl mx-auto content-grid">
+ <!-- Article section -->
+ <div class="md:col-span-2">
+ <!-- Breadcrumb -->
+ {{ partial "breadcrumb.html" . }}
+
+ <!-- Article header -->
+ {{ partial "article-header.html" . }}
+
+ <!-- Article content -->
+ <div class="prose prose-invert max-w-none mb-12">
+ {{ .Content }}
+ </div>
+ </div>
+
+ <!-- Sidebar -->
+ {{ partial "sidebar.html" . }}
+ </div>
+</div>
+{{ end }}
diff --git a/layouts/page.html b/layouts/page.html
deleted file mode 100644
index 44073b2..0000000
--- a/layouts/page.html
+++ /dev/null
@@ -1,9 +0,0 @@
-{{ define "main" }}
- {{ if eq .Params.type "photo" }}
- {{ partial "photo-article.html" . }}
- {{ else if eq .Section "is" }}
- {{ partial "static-page.html" . }}
- {{ else }}
- {{ partial "article-single.html" . }}
- {{ end }}
-{{ end }}
diff --git a/layouts/partials/article-card.html b/layouts/partials/article-card.html
new file mode 100644
index 0000000..d803464
--- /dev/null
+++ b/layouts/partials/article-card.html
@@ -0,0 +1,92 @@
+{{ $articleType := .Params.type | default "life" }}
+{{ $typeConfig := .Site.Params.articleTypes }}
+{{ $typeData := index $typeConfig $articleType }}
+{{ $excerpt := .Description | default .Summary }}
+
+{{ $imageURL := "" }}
+{{ $useDefaultThumbnail := false }}
+{{ if .Params.image }}
+ {{ $resource := .Resources.GetMatch .Params.image }}
+ {{ if $resource }}
+ {{ $imageURL = $resource.RelPermalink }}
+ {{ else }}
+ {{ $imageURL = .Params.image }}
+ {{ end }}
+{{ else }}
+ {{ $useDefaultThumbnail = true }}
+{{ end }}
+
+<article class="card group bg-bg">
+ <!-- Thumbnail -->
+ {{ if or $imageURL $useDefaultThumbnail }}
+ <a href="{{ .RelPermalink }}" class="block overflow-hidden bg-surface/50 relative" tabindex="-1">
+ {{ if $useDefaultThumbnail }}
+ <picture>
+ <source srcset="/images/default_thumbnail_light.png" media="(prefers-color-scheme: light)" />
+ <img
+ src="/images/default_thumbnail_dark.png"
+ alt="{{ .Title }}"
+ class="card-image group-hover:scale-105 transition-transform duration-200"
+ loading="lazy"
+ />
+ </picture>
+ {{ else }}
+ <img
+ src="{{ $imageURL }}"
+ alt="{{ .Title }}"
+ class="card-image group-hover:scale-105 transition-transform duration-200"
+ loading="lazy"
+ />
+ {{ end }}
+ <!-- Type badge pill overlay -->
+ {{ if $typeData }}
+ <div
+ class="absolute top-3 right-3 px-3 py-1.5 rounded-full text-xs font-semibold transition-opacity"
+ style="background-color: var(--type-{{ $articleType }}); color: var(--type-{{ $articleType }}-text);"
+ >
+ {{ i18n $articleType }}
+ </div>
+ {{ end }}
+ </a>
+ {{ end }}
+
+ <!-- Content -->
+ <div class="card-body">
+ <!-- Pinned badge -->
+ {{ if .Params.pinned }}
+ <div class="inline-flex items-center gap-1 px-2 py-1 rounded text-sm font-semibold" style="color: {{ .Site.Params.secondaryAccent }};">
+ 📌 PINNED
+ </div>
+ {{ end }}
+
+ <!-- Title -->
+ <h3 class="text-xl font-semibold">
+ <a href="{{ .RelPermalink }}" class="group-hover:text-accent transition-colors">
+ {{ .Title }}
+ </a>
+ </h3>
+
+ <!-- Metadata -->
+ <div class="flex flex-wrap items-center gap-3 text-sm text-text-dim">
+ <time datetime="{{ .PublishDate.Format "2006-01-02T15:04:05Z07:00" }}">
+ {{ .PublishDate.Format "Jan 2, 2006" }}
+ </time>
+ </div>
+
+ <!-- Excerpt -->
+ {{ if $excerpt }}
+ <p class="text-text-dim text-sm line-clamp-3 leading-relaxed">
+ {{ $excerpt | plainify }}
+ </p>
+ {{ end }}
+
+ <!-- CTA Button -->
+ <a
+ href="{{ .RelPermalink }}"
+ class="btn btn-sm mt-2"
+ >
+ {{ i18n "readMore" }}
+ <i data-feather="arrow-right" class="w-4 h-4 ml-2"></i>
+ </a>
+ </div>
+</article>
diff --git a/layouts/partials/article-header.html b/layouts/partials/article-header.html
new file mode 100644
index 0000000..94c78a5
--- /dev/null
+++ b/layouts/partials/article-header.html
@@ -0,0 +1,71 @@
+{{ $articleType := .Params.type | default "life" }}
+{{ $typeConfig := .Site.Params.articleTypes }}
+{{ $typeData := index $typeConfig $articleType }}
+{{ $hasType := .Params.type }}
+
+<div class="mb-8 pb-8 border-b border-border">
+ <!-- Title with accent corner -->
+ <div class="relative mb-6">
+ <!-- Vertical accent line (left side) -->
+ {{ if and $typeData $hasType }}
+ <div
+ class="absolute left-0 top-0 bottom-0 w-1 rounded-full"
+ style="background-color: var(--type-{{ $articleType }});"
+ ></div>
+ {{ end }}
+
+ <!-- Horizontal accent line (bottom, 50% width with fade) -->
+ {{ if and $typeData $hasType }}
+ <div
+ class="absolute left-0 bottom-0 h-1 rounded-full"
+ style="width: 50%; background: linear-gradient(to right, var(--type-{{ $articleType }}), var(--type-{{ $articleType }}) 0%, transparent 100%);"
+ ></div>
+ {{ end }}
+
+ <h1 class="text-4xl md:text-5xl font-bold text-accent pl-6 pt-2">
+ {{ .Title }}
+ </h1>
+ </div>
+
+ <!-- Metadata -->
+ <div class="flex flex-wrap items-center gap-4 text-sm text-text-dim">
+ <!-- Publish date -->
+ {{ if .PublishDate }}
+ <div class="flex items-center gap-1">
+ <i data-feather="calendar" class="w-4 h-4"></i>
+ <time datetime="{{ .PublishDate.Format "2006-01-02T15:04:05Z07:00" }}">
+ {{ .PublishDate.Format "Jan 2, 2006" }}
+ </time>
+ </div>
+ {{ end }}
+
+ <!-- Update date if different -->
+ {{ if .Lastmod }}
+ {{ $lastmodDate := .Lastmod.Format "2006-01-02" }}
+ {{ $pubDate := .PublishDate.Format "2006-01-02" }}
+ {{ if ne $lastmodDate $pubDate }}
+ <div class="flex items-center gap-1">
+ <i data-feather="edit-2" class="w-4 h-4"></i>
+ <time datetime="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}">
+ {{ .Lastmod.Format "Jan 2, 2006" }}
+ </time>
+ </div>
+ {{ end }}
+ {{ end }}
+
+ <!-- Reading time -->
+ {{ if .Site.Params.readingTime }}
+ <div class="flex items-center gap-1">
+ <i data-feather="clock" class="w-4 h-4"></i>
+ <span>{{ .ReadingTime }} {{ i18n "min" }} {{ i18n "readingTime" }}</span>
+ </div>
+ {{ end }}
+
+ <!-- Type badge -->
+ {{ if and $typeData $hasType }}
+ <span class="inline-flex items-center px-2.5 py-1 rounded text-sm font-mono font-semibold whitespace-nowrap transition-all duration-200" style="background-color: var(--type-{{ $articleType }}); border: 1px solid var(--type-{{ $articleType }}); color: var(--type-{{ $articleType }}-text);">
+ {{ i18n $articleType }}
+ </span>
+ {{ end }}
+ </div>
+</div>
diff --git a/layouts/partials/article-list-item.html b/layouts/partials/article-list-item.html
new file mode 100644
index 0000000..44cbf32
--- /dev/null
+++ b/layouts/partials/article-list-item.html
@@ -0,0 +1,95 @@
+{{/* Handle both calling conventions: dict with .Page/.side or direct page */}}
+{{ $page := . }}
+{{ $side := "left" }}
+
+{{ if reflect.IsMap . }}
+ {{ $page = .Page }}
+ {{ $side = .side | default "left" }}
+{{ end }}
+
+{{ $articleType := $page.Params.type | default "life" }}
+{{ $typeConfig := $page.Site.Params.articleTypes }}
+{{ $typeData := index $typeConfig $articleType }}
+{{ $excerpt := $page.Description | default $page.Summary }}
+
+{{ $imageURL := "" }}
+{{ $useDefaultThumbnail := false }}
+{{ if $page.Params.image }}
+ {{ $resource := $page.Resources.GetMatch $page.Params.image }}
+ {{ if $resource }}
+ {{ $imageURL = $resource.RelPermalink }}
+ {{ else }}
+ {{ $imageURL = $page.Params.image }}
+ {{ end }}
+{{ else }}
+ {{ $useDefaultThumbnail = true }}
+{{ end }}
+
+<li class="timeline-item timeline-item--{{ $side }}" style="--type-color: var(--type-{{ $articleType }});">
+ {{/* Decorative connector line */}}
+ <div class="timeline-connector" aria-hidden="true"
+ style="background-color: var(--type-{{ $articleType }});"></div>
+
+ {{/* Node on spine */}}
+ <div class="timeline-node" aria-hidden="true"
+ style="background-color: var(--type-{{ $articleType }}); box-shadow: 0 0 10px color-mix(in srgb, var(--type-{{ $articleType }}) 50%, transparent);"></div>
+
+ <article class="timeline-card group"
+ style="border-color: color-mix(in srgb, var(--type-{{ $articleType }}) 25%, transparent);
+ box-shadow: 0 0 18px color-mix(in srgb, var(--type-{{ $articleType }}) 10%, transparent);">
+
+ {{/* Thumbnail */}}
+ <a href="{{ $page.RelPermalink }}" class="timeline-thumb" tabindex="-1" aria-hidden="true">
+ {{ if $useDefaultThumbnail }}
+ <picture>
+ <source srcset="/images/default_thumbnail_light.png" media="(prefers-color-scheme: light)" />
+ <img src="/images/default_thumbnail_dark.png"
+ alt=""
+ loading="lazy" />
+ </picture>
+ {{ else }}
+ <img src="{{ $imageURL }}"
+ alt=""
+ loading="lazy" />
+ {{ end }}
+ </a>
+
+ {{/* Text body */}}
+ <div class="timeline-body">
+
+ {{/* Pinned badge */}}
+ {{ if $page.Params.pinned }}
+ <div class="timeline-pinned" style="color: {{ $page.Site.Params.secondaryAccent }};">
+ 📌 {{ i18n "pinned" | default "PINNED" }}
+ </div>
+ {{ end }}
+
+ {{/* Type + date meta row */}}
+ <div class="timeline-meta" style="color: var(--type-{{ $articleType }});">
+ {{ i18n $articleType | upper }}
+ <span class="timeline-meta-sep" aria-hidden="true">·</span>
+ <time class="timeline-date"
+ datetime="{{ $page.PublishDate.Format "2006-01-02T15:04:05Z07:00" }}">
+ {{ $page.PublishDate.Format "Jan 2, 2006" }}
+ </time>
+ </div>
+
+ {{/* Title */}}
+ <h3 class="timeline-title">
+ <a href="{{ $page.RelPermalink }}">{{ $page.Title }}</a>
+ </h3>
+
+ {{/* Excerpt */}}
+ {{ if $excerpt }}
+ <p class="timeline-excerpt">{{ $excerpt | plainify }}</p>
+ {{ end }}
+
+ {{/* CTA */}}
+ <a href="{{ $page.RelPermalink }}" class="btn btn-sm mt-auto">
+ {{ i18n "readMore" }}
+ <i data-feather="arrow-right" class="w-4 h-4 ml-2"></i>
+ </a>
+
+ </div>
+ </article>
+</li>
diff --git a/layouts/partials/article-nav.html b/layouts/partials/article-nav.html
new file mode 100644
index 0000000..8a118b3
--- /dev/null
+++ b/layouts/partials/article-nav.html
@@ -0,0 +1,46 @@
+{{ $page := .page }}
+{{ $variant := .variant | default "bottom" }}
+{{ $prev := $page.PrevInSection }}
+{{ $next := $page.NextInSection }}
+
+{{/* Shell prompt command varies by position */}}
+{{ $cmd := "ls ../" }}
+{{ if eq $variant "top" }}
+ {{ $cmd = "cd" }}
+{{ end }}
+
+<nav class="article-nav {{ if eq $variant "bottom" }}mt-8{{ else }}mb-8{{ end }}"
+ aria-label="Article navigation">
+ <p class="article-nav-prompt" aria-hidden="true">
+ [visitor@danix.xyz articles]$ {{ $cmd }}
+ </p>
+ <div class="article-nav-links">
+ {{/* ---- Previous (left side) ---- */}}
+ {{ if $prev }}
+ <a href="{{ $prev.Permalink }}"
+ class="article-nav-link truncate md:max-w-[45%] text-left"
+ rel="prev"
+ title="{{ $prev.Title }}">
+ ◄ {{ $prev.Title }}
+ </a>
+ {{ else }}
+ <span class="article-nav-placeholder text-left" aria-label="Beginning of articles">
+ ◄ (beginning)
+ </span>
+ {{ end }}
+
+ {{/* ---- Next (right side) ---- */}}
+ {{ if $next }}
+ <a href="{{ $next.Permalink }}"
+ class="article-nav-link truncate md:max-w-[45%] text-right"
+ rel="next"
+ title="{{ $next.Title }}">
+ {{ $next.Title }} ►
+ </a>
+ {{ else }}
+ <span class="article-nav-placeholder text-right" aria-label="End of articles">
+ (end) ►
+ </span>
+ {{ end }}
+ </div>
+</nav>
diff --git a/layouts/partials/article-types/life.html b/layouts/partials/article-types/life.html
new file mode 100644
index 0000000..01cd8b8
--- /dev/null
+++ b/layouts/partials/article-types/life.html
@@ -0,0 +1,3 @@
+<div class="prose prose-invert max-w-none mb-12">
+ {{ .Content }}
+</div>
diff --git a/layouts/partials/article-types/link.html b/layouts/partials/article-types/link.html
new file mode 100644
index 0000000..bbf7906
--- /dev/null
+++ b/layouts/partials/article-types/link.html
@@ -0,0 +1,17 @@
+<div class="mb-8 p-6 bg-surface/30 border border-accent/30 rounded-lg">
+ <a
+ href="{{ .Params.external_url }}"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="inline-flex items-center gap-2 px-6 py-3 bg-accent text-white rounded font-semibold hover:opacity-90 transition-opacity"
+ >
+ {{ .Params.link_title | default (i18n "readMore") }}
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
+ </svg>
+ </a>
+</div>
+
+<div class="prose prose-invert max-w-none mb-12">
+ {{ .Content }}
+</div>
diff --git a/layouts/partials/article-types/photo.html b/layouts/partials/article-types/photo.html
new file mode 100644
index 0000000..743e02a
--- /dev/null
+++ b/layouts/partials/article-types/photo.html
@@ -0,0 +1,19 @@
+{{ if .Params.featured_image }}
+<figure class="mb-8">
+ <img
+ src="{{ .Params.featured_image }}"
+ alt="{{ .Title }}"
+ loading="lazy"
+ class="w-full h-auto rounded-lg border border-border/30"
+ />
+ {{ if .Params.featured_image_caption }}
+ <figcaption class="bg-surface/30 p-4 text-sm text-text-dim rounded-b-lg">
+ {{ .Params.featured_image_caption }}
+ </figcaption>
+ {{ end }}
+</figure>
+{{ end }}
+
+<div class="prose prose-invert max-w-none mb-12">
+ {{ .Content }}
+</div>
diff --git a/layouts/partials/article-types/quote.html b/layouts/partials/article-types/quote.html
new file mode 100644
index 0000000..f27d189
--- /dev/null
+++ b/layouts/partials/article-types/quote.html
@@ -0,0 +1,15 @@
+<blockquote class="mb-8 pl-6 border-l-4 border-[--type-quote] italic text-2xl text-text">
+ "{{ .Params.quote_text }}"
+</blockquote>
+
+{{ if .Params.quote_author }}
+<div class="text-right text-text-dim mb-12">
+ — {{ .Params.quote_author }}
+</div>
+{{ end }}
+
+{{ if .Content }}
+<div class="prose prose-invert max-w-none">
+ {{ .Content }}
+</div>
+{{ end }}
diff --git a/layouts/partials/article-types/tech.html b/layouts/partials/article-types/tech.html
new file mode 100644
index 0000000..01cd8b8
--- /dev/null
+++ b/layouts/partials/article-types/tech.html
@@ -0,0 +1,3 @@
+<div class="prose prose-invert max-w-none mb-12">
+ {{ .Content }}
+</div>
diff --git a/layouts/partials/back-to-top.html b/layouts/partials/back-to-top.html
new file mode 100644
index 0000000..75095e3
--- /dev/null
+++ b/layouts/partials/back-to-top.html
@@ -0,0 +1,23 @@
+<div
+ x-data="{ visible: false }"
+ @scroll.window="visible = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) >= 0.33"
+>
+ <button
+ x-show="visible"
+ x-cloak
+ x-transition:enter="transition ease-out duration-300"
+ x-transition:enter-start="opacity-0 translate-y-4"
+ x-transition:enter-end="opacity-100 translate-y-0"
+ x-transition:leave="transition ease-in duration-200"
+ x-transition:leave-start="opacity-100 translate-y-0"
+ x-transition:leave-end="opacity-0 translate-y-4"
+ class="back-to-top"
+ aria-label="{{ i18n "back_to_top" | default "Back to top" }}"
+ type="button"
+ @click="window.scrollTo({ top: 0, behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth' })"
+ >
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
+ <polyline points="18 15 12 9 6 15"></polyline>
+ </svg>
+ </button>
+</div>
diff --git a/layouts/partials/breadcrumb-jsonld.html b/layouts/partials/breadcrumb-jsonld.html
new file mode 100644
index 0000000..95b5e69
--- /dev/null
+++ b/layouts/partials/breadcrumb-jsonld.html
@@ -0,0 +1,36 @@
+{{- $homeURL := absURL "/" -}}
+{{- $homeName := i18n "home" -}}
+{{- if eq .Lang "it" -}}
+ {{- $homeURL = absURL "/it/" -}}
+{{- end -}}
+<script type="application/ld+json">
+{
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ "itemListElement": [
+ {
+ "@type": "ListItem",
+ "position": 1,
+ "name": "{{ $homeName }}",
+ "item": "{{ $homeURL }}"
+ }
+ {{- $pos := 1 -}}
+ {{- range .Ancestors.Reverse -}}
+ {{- if ne .Kind "home" -}}
+ {{- $pos = add $pos 1 -}}
+ ,{
+ "@type": "ListItem",
+ "position": {{ $pos }},
+ "name": "{{ .Title }}",
+ "item": "{{ .Permalink }}"
+ }
+ {{- end -}}
+ {{- end -}}
+ ,{
+ "@type": "ListItem",
+ "position": {{ add $pos 1 }},
+ "name": "{{ .Title }}"
+ }
+ ]
+}
+</script>
diff --git a/layouts/partials/breadcrumb.html b/layouts/partials/breadcrumb.html
new file mode 100644
index 0000000..c15dfeb
--- /dev/null
+++ b/layouts/partials/breadcrumb.html
@@ -0,0 +1,16 @@
+{{ $homeLink := "/" }}
+{{ if eq .Lang "it" }}
+ {{ $homeLink = "/it/" }}
+{{ end }}
+
+<nav class="breadcrumb mb-6" aria-label="Breadcrumb">
+ <a href="{{ $homeLink }}">{{ i18n "home" }}</a>
+ {{ range .Ancestors.Reverse }}
+ {{ if ne .Kind "home" }}
+ <span class="breadcrumb-separator">/</span>
+ <a href="{{ .RelPermalink }}">{{ .Title }}</a>
+ {{ end }}
+ {{ end }}
+ <span class="breadcrumb-separator">/</span>
+ <span>{{ .Title }}</span>
+</nav>
diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html
new file mode 100644
index 0000000..1e50d6f
--- /dev/null
+++ b/layouts/partials/footer.html
@@ -0,0 +1,89 @@
+{{- $quotes := .Site.Data.quotes.quotes -}}
+
+<footer class="mt-16 frosted-bar border-t py-12 relative z-20">
+ <div class="container mx-auto px-4">
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8 mb-8">
+
+ <!-- Column 1: Fortune Cookie -->
+ <div>
+ <p class="font-mono text-xs text-text-dim mb-2">$ fortune</p>
+ <div id="fortune-quote" aria-live="polite" data-quotes='{{ $quotes | jsonify }}'>
+ <blockquote>
+ <p class="fortune-text font-mono text-sm text-text italic leading-relaxed">
+ "{{ (index $quotes 0).text }}"
+ </p>
+ <cite class="fortune-author font-mono text-xs text-text-dim not-italic mt-2 block text-right">
+ — {{ (index $quotes 0).author }}
+ </cite>
+ </blockquote>
+ </div>
+ </div>
+ <hr class="md:hidden border-border my-6" />
+
+ <!-- Column 2: Stack & Feature Badges -->
+ <div>
+ <p class="text-text-dim font-mono text-xs mb-1">{{ i18n "footer_built_with" }}</p>
+ <div class="flex flex-wrap gap-1.5 mb-3">
+ <span class="badge-footer-accent">Hugo</span>
+ <span class="badge-footer-accent">Tailwind CSS</span>
+ <span class="badge-footer-accent">Alpine.js</span>
+ <span class="badge-footer-accent">HTML5</span>
+ <span class="badge-footer-accent">CSS3</span>
+ <span class="badge-footer-accent">JavaScript</span>
+ <span class="badge-footer-accent">Claude Code</span>
+ </div>
+
+ <p class="text-text-dim font-mono text-xs mb-1">{{ i18n "footer_features" }}</p>
+ <div class="flex flex-wrap gap-1.5">
+ <a href="{{ .Site.LanguagePrefix }}/is/a11y-compliant/" class="badge-footer-accent2 hover:opacity-80 transition-opacity">WCAG 2.1 AA</a>
+ <span class="badge-footer-accent2">Open Source</span>
+ <span class="badge-footer-accent2">Privacy Friendly</span>
+ <span class="badge-footer-accent2">Keyboard Accessible</span>
+ <span class="badge-footer-accent2">Screen Reader Compatible</span>
+ </div>
+ </div>
+ <hr class="md:hidden border-border my-6" />
+
+ <!-- Column 3: About (Terminal Readout) -->
+ <div>
+ <dl class="space-y-1">
+ <div class="flex gap-2">
+ <dt class="text-text-dim font-mono text-xs w-20 shrink-0">{{ i18n "footer_about_name" }}:</dt>
+ <dd class="text-text font-mono text-xs">{{ .Site.Params.author }}</dd>
+ </div>
+ <div class="flex gap-2">
+ <dt class="text-text-dim font-mono text-xs w-20 shrink-0">{{ i18n "footer_about_role" }}:</dt>
+ <dd class="text-text font-mono text-xs">{{ i18n "footer_about_role_value" }}</dd>
+ </div>
+ <div class="flex gap-2">
+ <dt class="text-text-dim font-mono text-xs w-20 shrink-0">{{ i18n "footer_about_cert" }}:</dt>
+ <dd class="text-accent2 font-mono text-xs font-semibold">eJPT</dd>
+ </div>
+ <div class="flex gap-2">
+ <dt class="text-text-dim font-mono text-xs w-20 shrink-0">{{ i18n "footer_about_os" }}:</dt>
+ <dd class="text-text font-mono text-xs">{{ i18n "footer_about_os_value" }} <span class="text-text-dim">({{ i18n "footer_about_os_year" }})</span></dd>
+ </div>
+ <div class="flex gap-2">
+ <dt class="text-text-dim font-mono text-xs w-20 shrink-0">{{ i18n "footer_about_focus" }}:</dt>
+ <dd class="text-text font-mono text-xs">{{ i18n "footer_about_focus_value" }}</dd>
+ </div>
+ </dl>
+ </div>
+ </div>
+
+ <!-- Copyright Bar -->
+ <div class="pt-8 border-t border-border text-center text-xs text-text-dim space-y-1">
+ <p>
+ {{ i18n "footer_made_with" }} <span aria-hidden="true">❤️</span><span class="sr-only">{{ i18n "footer_love" }}</span>,
+ {{ i18n "footer_lack_of" }} <span aria-hidden="true">🛏️</span><span class="sr-only">{{ i18n "footer_sleep" }}</span>,
+ {{ i18n "footer_lots_of" }} <span aria-hidden="true">☕</span><span class="sr-only">{{ i18n "footer_coffee" }}</span>
+ {{ i18n "footer_by" }} <a href="{{ .Site.LanguagePrefix }}/is/" class="text-accent hover:text-accent2 transition-colors py-2 px-1">danix</a>
+ </p>
+ <p>&copy; {{ now.Year }} {{ .Site.Params.author }}. {{ i18n "allRightsReserved" }}</p>
+ </div>
+ </div>
+
+ <!-- Fortune.js: Pick a random quote on each page load -->
+ {{- $fortuneJS := resources.Get "js/fortune.js" | minify -}}
+ <script src="{{ $fortuneJS.RelPermalink }}"></script>
+</footer>
diff --git a/layouts/partials/form-components.html b/layouts/partials/form-components.html
new file mode 100644
index 0000000..6c6416b
--- /dev/null
+++ b/layouts/partials/form-components.html
@@ -0,0 +1,219 @@
+{{ define "form-components" }}
+
+<!-- ============================================
+ FORM INPUT EXAMPLES
+ ============================================ -->
+
+<section class="my-12 space-y-8">
+ <h2>Form Components</h2>
+
+ <!-- Basic Form Group -->
+ <div class="form-group">
+ <label for="input-text">Text Input</label>
+ <input type="text" id="input-text" class="form-input" placeholder="Enter text...">
+ </div>
+
+ <!-- Form Group with Error -->
+ <div class="form-group error">
+ <label for="input-email">Email (with error)</label>
+ <input type="email" id="input-email" class="form-input error" value="invalid-email">
+ <div class="form-error">{{ i18n "form_invalid_email" }}</div>
+ </div>
+
+ <!-- Form Group with Help Text -->
+ <div class="form-group">
+ <label for="input-password" class="">Password</label>
+ <input type="password" id="input-password" class="form-input" placeholder="••••••">
+ <div class="form-group-help">{{ i18n "form_password_help" | default "Must be at least 8 characters" }}</div>
+ </div>
+
+ <!-- Disabled Input -->
+ <div class="form-group">
+ <label for="input-disabled">Disabled Input</label>
+ <input type="text" id="input-disabled" class="form-input" value="Cannot edit" disabled>
+ </div>
+
+ <!-- ============================================
+ TEXTAREA EXAMPLES
+ ============================================ -->
+
+ <div class="form-group">
+ <label for="textarea-message">Message</label>
+ <textarea id="textarea-message" class="form-textarea" placeholder="Enter your message..."></textarea>
+ </div>
+
+ <!-- ============================================
+ SELECT EXAMPLES
+ ============================================ -->
+
+ <div class="form-group">
+ <label for="select-option">Select an option</label>
+ <select id="select-option" class="form-select">
+ <option>Choose...</option>
+ <option>Option 1</option>
+ <option>Option 2</option>
+ <option>Option 3</option>
+ </select>
+ </div>
+
+ <!-- ============================================
+ CHECKBOX EXAMPLES
+ ============================================ -->
+
+ <div class="form-group">
+ <label for="agree-terms" class="flex items-center gap-3 cursor-pointer">
+ <input type="checkbox" id="agree-terms" class="form-checkbox">
+ <span>{{ i18n "form_agree_terms" | default "I agree to the terms" }}</span>
+ </label>
+ </div>
+
+ <!-- Multiple Checkboxes -->
+ <div class="form-group space-y-2">
+ <p class="font-semibold">{{ i18n "form_select_interests" | default "Select your interests" }}</p>
+ <label for="interest-tech" class="flex items-center gap-3 cursor-pointer">
+ <input type="checkbox" id="interest-tech" class="form-checkbox" name="interests">
+ <span>{{ i18n "form_interest_tech" | default "Technology" }}</span>
+ </label>
+ <label for="interest-design" class="flex items-center gap-3 cursor-pointer">
+ <input type="checkbox" id="interest-design" class="form-checkbox" name="interests">
+ <span>{{ i18n "form_interest_design" | default "Design" }}</span>
+ </label>
+ </div>
+
+ <!-- ============================================
+ RADIO BUTTON EXAMPLES
+ ============================================ -->
+
+ <div class="form-group space-y-2">
+ <p class="font-semibold">{{ i18n "form_select_preference" | default "Select a preference" }}</p>
+ <label for="preference-option-a" class="flex items-center gap-3 cursor-pointer">
+ <input type="radio" id="preference-option-a" name="preference" class="form-radio">
+ <span>{{ i18n "form_option_a" | default "Option A" }}</span>
+ </label>
+ <label for="preference-option-b" class="flex items-center gap-3 cursor-pointer">
+ <input type="radio" id="preference-option-b" name="preference" class="form-radio">
+ <span>{{ i18n "form_option_b" | default "Option B" }}</span>
+ </label>
+ </div>
+
+ <!-- ============================================
+ FORM ROWS (MULTI-COLUMN)
+ ============================================ -->
+
+ <div class="form-row">
+ <div class="form-group">
+ <label for="first-name">{{ i18n "form_first_name" | default "First Name" }}</label>
+ <input type="text" id="first-name" class="form-input" placeholder="John">
+ </div>
+ <div class="form-group">
+ <label for="last-name">{{ i18n "form_last_name" | default "Last Name" }}</label>
+ <input type="text" id="last-name" class="form-input" placeholder="Doe">
+ </div>
+ </div>
+
+ <!-- ============================================
+ INLINE FORMS
+ ============================================ -->
+
+ <div class="form-inline">
+ <div class="form-group">
+ <label for="search-input">{{ i18n "form_search" | default "Search" }}</label>
+ <input type="text" id="search-input" class="form-input" placeholder="Type to search...">
+ </div>
+ <button class="btn btn-primary">{{ i18n "form_search_btn" | default "Search" }}</button>
+ </div>
+
+ <!-- ============================================
+ MODALS (DEMO BUTTONS)
+ ============================================ -->
+
+ <div class="space-y-3 mt-8">
+ <h3>Modal Examples</h3>
+ <button class="btn btn-primary" @click="showAlertModal = true">{{ i18n "form_open_alert" | default "Open Alert Modal" }}</button>
+ <button class="btn btn-secondary" @click="showConfirmModal = true">{{ i18n "form_open_confirm" | default "Open Confirm Modal" }}</button>
+ <button class="btn btn-outline" @click="showContentModal = true">{{ i18n "form_open_content" | default "Open Content Modal" }}</button>
+ </div>
+
+ <!-- ============================================
+ NOTIFICATIONS (DEMO BUTTONS)
+ ============================================ -->
+
+ <div class="space-y-3 mt-8">
+ <h3>Notifications</h3>
+ <button class="btn btn-primary btn-sm" @click="showToast('success')">Success Toast</button>
+ <button class="btn btn-secondary btn-sm" @click="showToast('error')">Error Toast</button>
+ <button class="btn btn-sm" style="background-color: #3b82f6; color: white;" @click="showToast('info')">Info Toast</button>
+ <button class="btn btn-sm" style="background-color: #f59e0b; color: white;" @click="showToast('warning')">Warning Toast</button>
+ </div>
+
+</section>
+
+<!-- ============================================
+ ALERT MODAL
+ ============================================ -->
+
+<div class="modal" :class="{ active: showAlertModal }" x-show="showAlertModal">
+ <div class="modal-backdrop" @click="showAlertModal = false" aria-hidden="true"></div>
+ <div class="modal-content modal-sm" role="dialog" aria-labelledby="alert-modal-title" aria-modal="true" @x-show.transition.in="createFocusTrap($el)">
+ <div class="modal-header">
+ <h3 class="modal-title" id="alert-modal-title">{{ i18n "form_alert_title" | default "Alert" }}</h3>
+ <div class="modal-close" @click="showAlertModal = false" aria-label="{{ i18n "closeMenu" }}"></div>
+ </div>
+ <div class="modal-body">
+ <p>{{ i18n "form_alert_message" | default "This is an alert modal. Click OK to dismiss." }}</p>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-primary" @click="showAlertModal = false">{{ i18n "form_ok" | default "OK" }}</button>
+ </div>
+ </div>
+</div>
+
+<!-- ============================================
+ CONFIRM MODAL
+ ============================================ -->
+
+<div class="modal" :class="{ active: showConfirmModal }" x-show="showConfirmModal">
+ <div class="modal-backdrop" @click="showConfirmModal = false" aria-hidden="true"></div>
+ <div class="modal-content modal-sm" role="dialog" aria-labelledby="confirm-modal-title" aria-modal="true" @x-show.transition.in="createFocusTrap($el)">
+ <div class="modal-header">
+ <h3 class="modal-title" id="confirm-modal-title">{{ i18n "form_confirm_title" | default "Confirm Action" }}</h3>
+ <div class="modal-close" @click="showConfirmModal = false" aria-label="{{ i18n "closeMenu" }}"></div>
+ </div>
+ <div class="modal-body">
+ <p>{{ i18n "form_confirm_message" | default "Are you sure you want to continue?" }}</p>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-outline" @click="showConfirmModal = false">{{ i18n "form_cancel" | default "Cancel" }}</button>
+ <button class="btn btn-primary" @click="handleConfirm()">{{ i18n "form_confirm" | default "Confirm" }}</button>
+ </div>
+ </div>
+</div>
+
+<!-- ============================================
+ CONTENT MODAL
+ ============================================ -->
+
+<div class="modal" :class="{ active: showContentModal }" x-show="showContentModal">
+ <div class="modal-backdrop" @click="showContentModal = false" aria-hidden="true"></div>
+ <div class="modal-content modal-md" role="dialog" aria-labelledby="content-modal-title" aria-modal="true" @x-show.transition.in="createFocusTrap($el)">
+ <div class="modal-header">
+ <h3 class="modal-title" id="content-modal-title">{{ i18n "form_content_title" | default "Modal with Content" }}</h3>
+ <div class="modal-close" @click="showContentModal = false" aria-label="{{ i18n "closeMenu" }}"></div>
+ </div>
+ <div class="modal-body">
+ <p>{{ i18n "form_content_message" | default "This modal contains detailed content. You can add forms, lists, or any HTML here." }}</p>
+ <div class="mt-4">
+ <div class="form-group">
+ <label for="modal-input">Input in Modal</label>
+ <input type="text" id="modal-input" class="form-input" placeholder="Type here...">
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button class="btn btn-outline" @click="showContentModal = false">{{ i18n "form_close" | default "Close" }}</button>
+ <button class="btn btn-primary">{{ i18n "form_save" | default "Save" }}</button>
+ </div>
+ </div>
+</div>
+
+{{ end }}
diff --git a/layouts/partials/hamburger-menu.html b/layouts/partials/hamburger-menu.html
new file mode 100644
index 0000000..5d8d8ed
--- /dev/null
+++ b/layouts/partials/hamburger-menu.html
@@ -0,0 +1,125 @@
+<!-- Mobile menu overlay (Alpine.js controlled) -->
+<div
+ x-cloak
+ x-data="{ menuOpen: false }"
+ @toggle-menu.window="menuOpen = !menuOpen"
+ @keydown.escape.window="menuOpen = false"
+ :class="{ 'opacity-0 invisible': !menuOpen, 'opacity-100 visible': menuOpen }"
+ class="fixed inset-0 bg-black/50 backdrop-blur transition-all duration-300 z-40"
+ :aria-hidden="!menuOpen"
+ @click="if ($event.target === $el) menuOpen = false"
+>
+ <div
+ class="fixed top-0 right-0 h-screen w-full max-w-sm bg-bg border-l border-border overflow-y-auto transform transition-transform duration-300 z-50"
+ :class="{ 'translate-x-full': !menuOpen, 'translate-x-0': menuOpen }"
+ >
+ <!-- Close button -->
+ <div class="flex items-center justify-between p-6 border-b border-border">
+ <span class="font-bold text-lg text-accent font-oxanium">Menu</span>
+ <button
+ @click="menuOpen = false"
+ aria-label="{{ i18n "closeMenu" }}"
+ class="p-2 hover:bg-surface rounded transition-colors"
+ >
+ <i data-feather="x" class="w-5 h-5"></i>
+ </button>
+ </div>
+
+ <!-- Menu items -->
+ <nav class="p-6" role="navigation" aria-label="{{ i18n "mainMenu" }}">
+ {{ $currentPath := strings.TrimSuffix "/" .RelPermalink }}
+ {{ range .Site.Menus.main }}
+ {{ $menuPath := strings.TrimSuffix "/" .URL }}
+ {{ $isActive := eq $menuPath $currentPath }}
+ <a
+ href="{{ .URL }}"
+ @click="menuOpen = false"
+ class="block py-4 text-lg font-medium transition-colors border-b border-border/30 {{ if $isActive }}text-accent font-bold{{ else }}hover:text-accent{{ end }}"
+ {{ if $isActive }}aria-current="page"{{ end }}
+ >
+ {{ i18n .Name }}
+ </a>
+ {{ end }}
+ </nav>
+
+ <!-- Mobile search bar -->
+ <div class="p-6 border-b border-border" x-data="mobileSearch()">
+ <label for="search-input-mobile" class="sr-only">
+ {{ i18n "searchPlaceholder" }}
+ </label>
+ <input
+ id="search-input-mobile"
+ type="text"
+ :value="searchQuery"
+ @input="filterArticles($el.value); ensureIndexLoaded()"
+ placeholder="{{ i18n "searchPlaceholder" }}"
+ class="w-full px-4 py-2 border-2 border-border rounded focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent bg-bg text-text text-sm"
+ aria-describedby="mobile-search-results"
+ />
+
+ <!-- Mobile search results -->
+ <div id="mobile-search-results" class="mt-3 space-y-2" x-show="filteredArticles.length > 0" role="region" aria-live="polite">
+ <template x-for="article in filteredArticles" :key="article.url">
+ <div class="p-3 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors rounded text-sm">
+ <a :href="article.url" @click="menuOpen = false" class="block focus:outline-none focus:ring-2 focus:ring-accent rounded px-1 py-1">
+ <h4 class="font-bold text-accent" x-text="article.title"></h4>
+ <p class="text-xs text-text-dim mt-0.5" x-text="article.date"></p>
+ </a>
+ </div>
+ </template>
+ </div>
+
+ <!-- Empty state -->
+ <div
+ x-show="searchQuery && filteredArticles.length === 0"
+ class="mt-3 text-sm text-text-dim"
+ role="status"
+ >
+ {{ i18n "noSearchResults" }}
+ </div>
+ </div>
+
+ <!-- Language switcher -->
+ <div class="p-6">
+ <div class="text-sm text-text-dim mb-3">{{ i18n "language" }}</div>
+ <div class="flex gap-2">
+ {{ $currentLang := .Page.Language }}
+ {{ $currentPath := .RelPermalink }}
+ {{ range .Site.Languages }}
+ {{ $langCode := .Lang }}
+ {{ $langName := .LanguageName }}
+ {{ $current := eq $langCode $currentLang }}
+ <!-- Build the translated URL by replacing language prefix -->
+ {{ $url := $currentPath }}
+ {{ if eq $langCode "en" }}
+ {{ if hasPrefix $currentPath "/it/" }}
+ {{ $url = strings.TrimPrefix "/it" $currentPath }}
+ {{ end }}
+ {{ else }}
+ {{ if not (hasPrefix $currentPath "/it/") }}
+ {{ $url = printf "/it%s" $currentPath }}
+ {{ end }}
+ {{ end }}
+ <a
+ href="{{ $url }}"
+ @click="menuOpen = false"
+ class="flex-1 py-2 px-3 text-center rounded transition-colors {{ if $current }}bg-accent text-white{{ else }}bg-surface hover:bg-surface/80{{ end }}"
+ >
+ {{ $langName }}
+ </a>
+ {{ end }}
+ </div>
+ </div>
+
+ </div>
+</div>
+
+<script>
+ // Close menu before page navigation to prevent flicker
+ window.addEventListener('beforeunload', () => {
+ const overlay = document.querySelector('[x-data*="menuOpen"]');
+ if (overlay && overlay.__x) {
+ overlay.__x.$data.menuOpen = false;
+ }
+ });
+</script>
diff --git a/layouts/partials/head-meta.html b/layouts/partials/head-meta.html
new file mode 100644
index 0000000..34a60e2
--- /dev/null
+++ b/layouts/partials/head-meta.html
@@ -0,0 +1,57 @@
+{{/* description: per-page excerpt wins over site-wide description */}}
+{{ $description := .Site.Language.Params.siteDescription }}
+{{ with .Params.excerpt }}{{ $description = . }}{{ end }}
+
+{{/* og:type: "article" only for single content pages that have a date */}}
+{{ $ogType := "website" }}
+{{ if and (eq .Kind "page") .Date }}{{ $ogType = "article" }}{{ end }}
+
+{{/* og:image: page image wins; fall back to lamp for homepage, dark thumbnail for articles */}}
+{{ $defaultImage := "images/default_thumbnail_dark.png" }}
+{{ if .IsHome }}{{ $defaultImage = "images/lampD.png" }}{{ end }}
+{{ $ogImage := printf "%s%s" .Site.BaseURL $defaultImage }}
+{{ with .Params.image }}{{ $ogImage = printf "%s%s" $.Site.BaseURL (strings.TrimLeft "/" .) }}{{ end }}
+
+{{/* author: page-level param wins; fall back to site param */}}
+{{ $author := .Site.Params.author }}
+{{ with .Params.author }}{{ $author = . }}{{ end }}
+
+<meta name="description" content="{{ $description }}">
+
+<meta property="og:title" content="{{ .Title }}">
+<meta property="og:description" content="{{ $description }}">
+<meta property="og:type" content="{{ $ogType }}">
+<meta property="og:url" content="{{ .Permalink }}">
+<meta property="og:site_name" content="{{ .Site.Title }}">
+<meta property="og:locale" content="{{ .Site.Language.Params.locale }}">
+<meta property="og:image" content="{{ $ogImage }}">
+
+{{ if eq $ogType "article" }}
+<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
+<meta property="article:author" content="{{ $author }}">
+{{ end }}
+
+<meta name="twitter:card" content="summary_large_image">
+<meta name="twitter:title" content="{{ .Title }}">
+<meta name="twitter:description" content="{{ $description }}">
+<meta name="twitter:image" content="{{ $ogImage }}">
+{{ with .Site.Params.twitterHandle }}
+<meta name="twitter:site" content="@{{ . }}">
+{{ end }}
+
+{{/* SEO: Self-referencing canonical */}}
+<link rel="canonical" href="{{ .Permalink }}">
+
+{{/* SEO: hreflang alternates for all language variants */}}
+{{ range .AllTranslations }}
+<link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
+{{ end }}
+{{/* x-default: EN version (served at root /) */}}
+{{ range .AllTranslations }}
+ {{ if eq .Language.Lang "en" }}
+<link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
+ {{ end }}
+{{ end }}
+
+{{/* Structured data: BreadcrumbList JSON-LD (single pages and named sections) */}}
+{{ if or (eq .Kind "page") (eq .Kind "section") }}{{ partial "breadcrumb-jsonld.html" . }}{{ end }}
diff --git a/layouts/partials/header.html b/layouts/partials/header.html
new file mode 100644
index 0000000..914e645
--- /dev/null
+++ b/layouts/partials/header.html
@@ -0,0 +1,94 @@
+<header class="fixed top-0 left-0 right-0 z-50 frosted-bar border-b">
+ <nav class="container mx-auto px-4 py-4 flex items-center justify-between">
+ <!-- Logo and Site Name -->
+ {{ $homeLink := "/" }}
+ {{ if eq .Lang "it" }}
+ {{ $homeLink = "/it/" }}
+ {{ end }}
+ <a href="{{ $homeLink }}" class="flex items-center gap-2 hover:opacity-80 transition-opacity">
+ <img src="/images/lampD.png" alt="Logo" style="width: 40px; height: 40px; max-width: none;" class="flex-shrink-0">
+ <span class="hidden md:inline font-bold text-lg text-accent font-oxanium">danix.xyz</span>
+ </a>
+
+ <!-- Desktop menu (hidden on mobile) -->
+ <div class="hidden md:flex items-center gap-8">
+ {{ $currentPath := strings.TrimSuffix "/" .RelPermalink }}
+ {{ range .Site.Menus.main }}
+ {{ $menuPath := strings.TrimSuffix "/" .URL }}
+ {{ $isActive := eq $menuPath $currentPath }}
+ <a
+ href="{{ .URL }}"
+ class="text-sm transition-colors {{ if $isActive }}text-accent font-bold{{ else }}hover:text-accent{{ end }}"
+ {{ if $isActive }}aria-current="page"{{ end }}
+ >
+ {{ i18n .Name }}
+ </a>
+ {{ end }}
+ </div>
+
+ <!-- Right side controls: Search, Language, Theme, Menu -->
+ <div class="flex items-center gap-4 md:gap-6">
+ <!-- Search button (desktop only) -->
+ <button
+ x-data
+ @click="$dispatch('open-search')"
+ aria-label="{{ i18n "searchArticles" }}"
+ class="hidden md:flex p-2 rounded hover:bg-surface transition-colors focus:outline-none focus:ring-2 focus:ring-accent"
+ >
+ <i data-feather="search" class="w-5 h-5" aria-hidden="true"></i>
+ </button>
+
+ <!-- Language switcher (desktop) -->
+ <div class="hidden md:flex gap-2">
+ {{ $currentLang := .Page.Language }}
+ {{ $currentPath := .RelPermalink }}
+ {{ range .Site.Languages }}
+ {{ $langCode := .Lang }}
+ {{ $langName := .LanguageName }}
+ {{ $current := eq $langCode $currentLang }}
+ <!-- Build the translated URL by replacing language prefix -->
+ {{ $url := $currentPath }}
+ {{ if eq $langCode "en" }}
+ {{ if hasPrefix $currentPath "/it/" }}
+ {{ $url = strings.TrimPrefix "/it" $currentPath }}
+ {{ end }}
+ {{ else }}
+ {{ if not (hasPrefix $currentPath "/it/") }}
+ {{ $url = printf "/it%s" $currentPath }}
+ {{ end }}
+ {{ end }}
+ <a
+ href="{{ $url }}"
+ class="px-2 py-1 text-sm rounded transition-colors {{ if $current }}bg-accent text-white{{ else }}hover:bg-surface{{ end }}"
+ >
+ {{ $langName }}
+ </a>
+ {{ end }}
+ </div>
+
+ <!-- Theme toggle button -->
+ <button
+ id="theme-toggle"
+ aria-label="{{ i18n "toggleTheme" }}"
+ class="p-2 rounded hover:bg-surface transition-colors"
+ >
+ <i id="theme-icon-sun" data-feather="sun" class="w-5 h-5" aria-hidden="true"></i>
+ <i id="theme-icon-moon" data-feather="moon" class="w-5 h-5" aria-hidden="true"></i>
+ </button>
+
+ <!-- Hamburger menu button (mobile only) -->
+ <button
+ x-data
+ @click="$dispatch('toggle-menu')"
+ aria-label="{{ i18n "toggleMenu" }}"
+ aria-controls="hamburger-menu"
+ class="md:hidden p-2 rounded hover:bg-surface transition-colors"
+ >
+ <i data-feather="menu" class="w-5 h-5"></i>
+ </button>
+ </div>
+ </nav>
+
+ <!-- Mobile hamburger overlay menu -->
+ {{ partial "hamburger-menu.html" . }}
+</header>
diff --git a/layouts/partials/search-modal.html b/layouts/partials/search-modal.html
new file mode 100644
index 0000000..6b96b3a
--- /dev/null
+++ b/layouts/partials/search-modal.html
@@ -0,0 +1,87 @@
+<!-- Desktop Search Modal (hidden on mobile, shown via Alpine) -->
+<div
+ x-cloak
+ x-data="searchOverlay()"
+ @keydown.escape.window="handleEscape($event)"
+ @open-search.window="open()"
+ class="fixed inset-0 z-50"
+ :class="{ 'flex items-center justify-center': isOpen, 'hidden': !isOpen }"
+ x-show="isOpen"
+>
+ <!-- Overlay backdrop -->
+ <div
+ class="absolute inset-0 bg-black/50"
+ @click="close()"
+ aria-hidden="true"
+ ></div>
+
+ <!-- Modal content -->
+ <div
+ class="relative bg-bg border-2 border-accent rounded-lg shadow-xl max-w-2xl mx-4 w-full"
+ role="dialog"
+ aria-labelledby="search-modal-title"
+ aria-modal="true"
+ >
+ <!-- Header with close button -->
+ <div class="flex items-center justify-between p-6 border-b border-border">
+ <h2 id="search-modal-title" class="text-xl font-bold text-accent">
+ {{ i18n "searchArticles" }}
+ </h2>
+ <button
+ @click="close()"
+ aria-label="Close search"
+ class="p-2 rounded hover:bg-surface transition-colors focus:outline-none focus:ring-2 focus:ring-accent"
+ >
+ <i data-feather="x" class="w-5 h-5" aria-hidden="true"></i>
+ </button>
+ </div>
+
+ <!-- Search input -->
+ <div class="p-6 border-b border-border">
+ <label for="search-input-desktop" class="sr-only">
+ {{ i18n "searchPlaceholder" }}
+ </label>
+ <input
+ id="search-input-desktop"
+ type="text"
+ :value="searchQuery"
+ @input="filterArticles($el.value)"
+ placeholder="{{ i18n "searchPlaceholder" }}"
+ class="w-full px-4 py-3 border-2 border-border rounded focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent bg-bg text-text"
+ aria-describedby="search-results"
+ />
+ </div>
+
+ <!-- Results container -->
+ <div id="search-results" class="max-h-96 overflow-y-auto p-6">
+ <!-- Results list -->
+ <div x-show="filteredArticles.length > 0" class="space-y-3" role="region" aria-live="polite">
+ <template x-for="article in filteredArticles" :key="article.url">
+ <div class="p-4 border-l-4 border-accent bg-bg/50 hover:bg-bg/70 transition-colors rounded">
+ <a :href="article.url" class="block focus:outline-none focus:ring-2 focus:ring-accent rounded px-2 py-1">
+ <h3 class="font-bold text-accent hover:underline" x-text="article.title"></h3>
+ <p class="text-sm text-text-dim mt-1" x-text="article.date"></p>
+ </a>
+ </div>
+ </template>
+ </div>
+
+ <!-- Empty state -->
+ <div
+ x-show="searchQuery && filteredArticles.length === 0"
+ class="text-center py-8 text-text-dim"
+ role="status"
+ >
+ {{ i18n "noSearchResults" }}
+ </div>
+
+ <!-- No query state -->
+ <div
+ x-show="!searchQuery"
+ class="text-center py-8 text-text-dim"
+ >
+ {{ i18n "searchPlaceholder" }}
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/layouts/partials/sidebar.html b/layouts/partials/sidebar.html
new file mode 100644
index 0000000..a2225f1
--- /dev/null
+++ b/layouts/partials/sidebar.html
@@ -0,0 +1,54 @@
+<aside class="order-last md:order-none md:col-span-1">
+ <!-- Author info widget (optional - can be expanded) -->
+ <div class="sidebar-widget">
+ <p class="sidebar-widget-label"># {{ i18n "author" }}</p>
+ <p class="text-text-dim text-sm leading-relaxed">
+ {{ .Site.Params.author }}
+ </p>
+ </div>
+
+ <hr class="sidebar-hr">
+
+ <!-- Social sharing widget -->
+ {{ partial "social-share.html" (dict "page" . "mode" "sidebar") }}
+
+ <!-- Related posts widget (articles only) -->
+ {{ if and .Site.Params.relatedPosts (eq .Section "articles") }}
+ {{ $related := first 5 (.Site.RegularPages.Related .) }}
+ <hr class="sidebar-hr">
+ <div class="sidebar-widget">
+ <p class="sidebar-widget-label"># {{ i18n "relatedPosts" }}</p>
+ {{ if $related }}
+ <ul class="space-y-3">
+ {{ range $related }}
+ {{ $articleType := .Params.type | default "life" }}
+ {{ $excerpt := .Description | default .Summary }}
+ <li
+ class="pl-3 border-l-2"
+ style="border-color: var(--type-{{ $articleType }});"
+ >
+ <a href="{{ .RelPermalink }}" class="text-sm hover:text-accent transition-colors leading-snug block">
+ {{ .Title }}
+ <time class="text-text-dim/60 ml-1" datetime="{{ .PublishDate.Format "2006-01-02T15:04:05Z07:00" }}">
+ · {{ .PublishDate.Format "Jan 2006" }}
+ </time>
+ </a>
+ {{ if $excerpt }}
+ <p class="text-xs text-text-dim/70 leading-snug mt-0.5 line-clamp-1">
+ {{ $excerpt | plainify | truncate 60 }}
+ </p>
+ {{ end }}
+ </li>
+ {{ end }}
+ </ul>
+ {{ else }}
+ <p class="text-sm text-text-dim">{{ i18n "noRelated" }}</p>
+ {{ end }}
+ </div>
+ {{ end }}
+
+ <hr class="sidebar-hr">
+
+ <!-- Tag Cloud Widget -->
+ {{ partial "tag-cloud.html" (dict "page" . "showCount" false "wrapInWidget" true "maxTags" 15) }}
+</aside>
diff --git a/layouts/partials/social-share.html b/layouts/partials/social-share.html
new file mode 100644
index 0000000..9e9be43
--- /dev/null
+++ b/layouts/partials/social-share.html
@@ -0,0 +1,123 @@
+{{ $page := .page }}
+{{ $mode := .mode | default "sidebar" }}
+{{ $url := $page.Permalink | urlquery }}
+{{ $title := $page.Title | urlquery }}
+
+{{ $gridClass := "share-grid gap-1" }}
+{{ if eq $mode "inline" }}
+ {{ $gridClass = "flex flex-wrap justify-center gap-1" }}
+{{ end }}
+
+<div class="sidebar-widget">
+ <p class="sidebar-widget-label"># {{ i18n "share" }}</p>
+ <nav aria-label="{{ i18n "share" }}">
+ <div class="{{ $gridClass }}">
+
+ <!-- Copy link -->
+ <div x-data="{ copied: false }">
+ <button
+ @click="navigator.clipboard.writeText('{{ $page.Permalink }}').then(() => { copied = true; setTimeout(() => copied = false, 2000) })"
+ :aria-label="copied ? '{{ i18n "linkCopied" }}' : '{{ i18n "copyLink" }}'"
+ :class="copied ? 'btn-share btn-share--copied' : 'btn-share'"
+ >
+ <i x-show="!copied" data-feather="copy" aria-hidden="true"></i>
+ <i x-show="copied" data-feather="check" aria-hidden="true"></i>
+ </button>
+ </div>
+
+ <!-- Email -->
+ <a
+ href="mailto:?subject={{ $title }}&body={{ $title }}%0A%0A{{ $page.Permalink | urlquery }}"
+ aria-label="{{ i18n "shareViaEmail" }}"
+ class="btn-share"
+ >
+ <i data-feather="mail" aria-hidden="true"></i>
+ </a>
+
+ <!-- Facebook -->
+ <a
+ href="https://www.facebook.com/sharer/sharer.php?u={{ $url }}"
+ target="_blank" rel="noopener noreferrer"
+ aria-label="{{ i18n "facebook" }}"
+ class="btn-share"
+ >
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
+ <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/>
+ </svg>
+ </a>
+
+ <!-- Twitter / X -->
+ <a
+ href="https://twitter.com/intent/tweet?url={{ $url }}&text={{ $title }}"
+ target="_blank" rel="noopener noreferrer"
+ aria-label="{{ i18n "twitter" }}"
+ class="btn-share"
+ >
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true" focusable="false">
+ <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/>
+ </svg>
+ </a>
+
+ <!-- Reddit -->
+ <a
+ href="https://www.reddit.com/submit?url={{ $url }}&title={{ $title }}"
+ target="_blank" rel="noopener noreferrer"
+ aria-label="{{ i18n "reddit" }}"
+ class="btn-share"
+ >
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true" focusable="false">
+ <path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/>
+ </svg>
+ </a>
+
+ <!-- Pinterest -->
+ <a
+ href="https://pinterest.com/pin/create/button/?url={{ $url }}&description={{ $title }}"
+ target="_blank" rel="noopener noreferrer"
+ aria-label="{{ i18n "pinterest" }}"
+ class="btn-share"
+ >
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true" focusable="false">
+ <path d="M12 0C5.373 0 0 5.373 0 12c0 5.084 3.163 9.426 7.627 11.174-.105-.949-.2-2.405.042-3.441.218-.937 1.407-5.965 1.407-5.965s-.359-.719-.359-1.782c0-1.668.967-2.914 2.171-2.914 1.023 0 1.518.769 1.518 1.69 0 1.029-.655 2.568-.994 3.995-.283 1.194.599 2.169 1.777 2.169 2.133 0 3.772-2.249 3.772-5.495 0-2.873-2.064-4.882-5.012-4.882-3.414 0-5.418 2.561-5.418 5.207 0 1.031.397 2.138.893 2.738a.36.36 0 0 1 .083.345l-.333 1.36c-.053.22-.174.267-.402.161-1.499-.698-2.436-2.889-2.436-4.649 0-3.785 2.75-7.262 7.929-7.262 4.163 0 7.398 2.967 7.398 6.931 0 4.136-2.607 7.464-6.227 7.464-1.216 0-2.359-.632-2.75-1.378l-.748 2.853c-.271 1.043-1.002 2.35-1.492 3.146C9.57 23.812 10.763 24 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0z"/>
+ </svg>
+ </a>
+
+ <!-- WhatsApp -->
+ <a
+ href="https://api.whatsapp.com/send?text={{ $title }}%20{{ $page.Permalink | urlquery }}"
+ target="_blank" rel="noopener noreferrer"
+ aria-label="{{ i18n "whatsapp" }}"
+ class="btn-share"
+ >
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true" focusable="false">
+ <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413z"/>
+ </svg>
+ </a>
+
+ <!-- Telegram -->
+ <a
+ href="https://t.me/share/url?url={{ $url }}&text={{ $title }}"
+ target="_blank" rel="noopener noreferrer"
+ aria-label="{{ i18n "telegram" }}"
+ class="btn-share"
+ >
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true" focusable="false">
+ <path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/>
+ </svg>
+ </a>
+
+ <!-- Signal -->
+ <a
+ href="https://signal.me/#p/{{ $page.Permalink | urlquery }}"
+ target="_blank" rel="noopener noreferrer"
+ aria-label="{{ i18n "signal" }}"
+ class="btn-share"
+ >
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none" aria-hidden="true" focusable="false">
+ <path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm.157 5.32a6.68 6.68 0 0 1 6.143 4.032l.734-.267.55 1.513-.717.261a6.71 6.71 0 0 1 .133 1.327 6.68 6.68 0 0 1-.176 1.51l.69.252-.55 1.513-.707-.258A6.68 6.68 0 0 1 12 18.68a6.68 6.68 0 0 1-6.294-4.455l-.66.24-.55-1.513.677-.247A6.71 6.71 0 0 1 5 12.187a6.71 6.71 0 0 1 .16-1.438l-.698-.254.55-1.513.725.264A6.68 6.68 0 0 1 12.157 5.32zM12 7.2a4.8 4.8 0 1 0 0 9.6A4.8 4.8 0 0 0 12 7.2z"/>
+ </svg>
+ </a>
+
+ </div>
+ </nav>
+</div>
diff --git a/layouts/partials/tag-cloud.html b/layouts/partials/tag-cloud.html
new file mode 100644
index 0000000..b896e8d
--- /dev/null
+++ b/layouts/partials/tag-cloud.html
@@ -0,0 +1,90 @@
+{{/* tag-cloud.html
+ Reusable tag cloud partial for homepage, sidebar, and 404 pages.
+
+ Params (dict):
+ page Page required — calling page context (provides .Site.Taxonomies.tags, .Lang)
+ showCount bool optional — show post count per tag (default true)
+ heading string optional — heading text override (default: i18n "tagCloud")
+ headingLevel string optional — h2|h3|p for non-widget mode (default "h2")
+ wrapInWidget bool optional — wrap in .sidebar-widget for sidebar placement (default false)
+ maxTags int optional — max tags to show, 0 = all (default 0)
+*/}}
+
+{{- $page := .page -}}
+{{- $showCount := .showCount | default true -}}
+{{- $heading := .heading | default (i18n "tagCloud") -}}
+{{- $headingLevel := .headingLevel -}}
+{{- $wrapInWidget := .wrapInWidget | default false -}}
+{{- $maxTags := .maxTags | default 0 -}}
+
+{{- $tags := $page.Site.Taxonomies.tags -}}
+
+{{/* Early exit if no tags */}}
+{{- if $tags -}}
+
+{{/* Compute max count for continuous scaling */}}
+{{- $maxCount := 0 -}}
+{{- range $tags -}}
+ {{- if gt .Count $maxCount -}}{{- $maxCount = .Count -}}{{- end -}}
+{{- end -}}
+
+{{/* Ordered tag list (descending by count) */}}
+{{- $orderedTags := $tags.ByCount -}}
+{{- if gt $maxTags 0 -}}
+ {{- $orderedTags = first $maxTags $orderedTags -}}
+{{- end -}}
+
+{{/* Render based on placement mode */}}
+{{- if $wrapInWidget -}}
+<div class="sidebar-widget">
+ <p class="sidebar-widget-label"># {{ i18n "topTags" }}</p>
+ <nav aria-label="{{ i18n "exploreTopics" }}">
+ <div class="tag-cloud" data-tag-cloud>
+{{- else -}}
+<section {{- if $headingLevel }} aria-labelledby="tag-cloud-heading"{{ end }}>
+ {{- if $headingLevel -}}
+ {{- if eq $headingLevel "h2" -}}
+ <h2 id="tag-cloud-heading" class="text-lg font-semibold text-accent mb-4">{{ $heading }}</h2>
+ {{- else if eq $headingLevel "h3" -}}
+ <h3 id="tag-cloud-heading" class="text-lg font-semibold text-accent mb-4">{{ $heading }}</h3>
+ {{- else -}}
+ <p id="tag-cloud-heading" class="text-lg font-semibold text-accent mb-4">{{ $heading }}</p>
+ {{- end -}}
+ {{- end -}}
+ <nav aria-label="{{ i18n "exploreTopics" }}">
+ <div class="tag-cloud" data-tag-cloud>
+{{- end -}}
+
+ {{- range $orderedTags -}}
+ {{- $count := .Count -}}
+ {{- $ratio := (div (float $count) (float $maxCount)) -}}
+ {{- $size := (add 0.6 (mul $ratio 1.2)) -}}
+ {{- $opacity := (add 0.7 (mul $ratio 0.3)) -}}
+ <a
+ href="{{ .Page.RelPermalink }}"
+ class="tag-cloud-link"
+ data-weight="{{ printf "%.4f" $ratio }}"
+ {{- if ge $ratio 0.5 }}
+ style="font-size: {{ $size }}rem; color: var(--accent); opacity: {{ $opacity }};"
+ {{- else }}
+ style="font-size: {{ $size }}rem; color: var(--text-dim); opacity: {{ $opacity }};"
+ {{- end }}
+ aria-label="{{ .Name }}{{- if $showCount }} ({{ i18n "postCount" $count }}){{- end -}}"
+ >
+ {{- .Name -}}
+ {{- if $showCount -}}
+ <span class="tag-cloud-count" aria-hidden="true">{{ $count }}</span>
+ {{- end -}}
+ </a>
+ {{- end -}}
+
+ </div>
+ </nav>
+
+{{- if $wrapInWidget -}}
+</div>
+{{- else -}}
+</section>
+{{- end -}}
+
+{{- end -}}{{/* end if $tags */}}
diff --git a/layouts/partials/toast-container.html b/layouts/partials/toast-container.html
new file mode 100644
index 0000000..7bbd3c0
--- /dev/null
+++ b/layouts/partials/toast-container.html
@@ -0,0 +1,13 @@
+{{ define "toast-container" }}
+
+<!-- Toast notification container with Alpine.js integration -->
+<div class="toast-container" x-data="formComponentsData()">
+ <template x-for="toast in toasts" :key="toast.id">
+ <div class="toast" :class="`toast-${toast.type}`" x-show="toasts.length > 0">
+ <span x-text="toast.message"></span>
+ <button class="toast-close" @click="removeToast(toast.id)" type="button" aria-label="Close notification"></button>
+ </div>
+ </template>
+</div>
+
+{{ end }}
diff --git a/layouts/repository/single.html b/layouts/repository/single.html
new file mode 100644
index 0000000..ce2c305
--- /dev/null
+++ b/layouts/repository/single.html
@@ -0,0 +1,77 @@
+{{ define "main" }}
+<article class="mx-auto px-4 py-12 max-w-7xl">
+ <div class="content-grid">
+ <!-- Breadcrumb -->
+ {{ partial "breadcrumb.html" . }}
+
+ <!-- Page Title (Hero) -->
+ <div class="mb-12">
+ <h1 class="text-5xl md:text-6xl font-bold mb-4 text-accent animate-fade-in">
+ {{ i18n "repositoryTitle" }}
+ </h1>
+ <p class="text-xl text-text-dim animate-fade-in-delay">
+ {{ i18n "repositorySubtitle" }}
+ </p>
+ </div>
+
+ <!-- Page Content (markdown sections) -->
+ <div class="prose dark:prose-invert max-w-none mb-12">
+ {{ .Content }}
+ </div>
+
+ <!-- Social sharing (inline mode) -->
+ <div class="mb-12">
+ {{ partial "social-share.html" (dict "page" . "mode" "inline") }}
+ </div>
+
+ <!-- Repository Cards Section -->
+ <section class="mt-16">
+ <h2 class="text-3xl font-bold mb-8">{{ i18n "githubReposTitle" }}</h2>
+
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
+ {{ range $.Site.Data.repos.repos }}
+ <div class="border border-border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
+ <!-- Card Image -->
+ <img
+ src="{{ .image }}"
+ alt="{{ .name }}"
+ class="w-full h-48 object-cover"
+ loading="lazy"
+ />
+
+ <!-- Card Content -->
+ <div class="p-6">
+ <h3 class="text-xl font-bold mb-2">{{ .name }}</h3>
+
+ <p class="text-text-dim mb-4">
+ {{ i18n .description_key }}
+ </p>
+
+ <!-- Tags -->
+ {{ if .tags }}
+ <div class="flex flex-wrap gap-2 mb-4">
+ {{ range .tags }}
+ <span class="text-xs px-2 py-1 bg-border/20 rounded">
+ {{ . }}
+ </span>
+ {{ end }}
+ </div>
+ {{ end }}
+
+ <!-- GitHub Link -->
+ <a
+ href="{{ .github_url }}"
+ target="_blank"
+ rel="noopener noreferrer"
+ class="btn btn-primary w-full text-center"
+ >
+ {{ i18n "visitGithub" }} →
+ </a>
+ </div>
+ </div>
+ {{ end }}
+ </div>
+ </section>
+ </div>
+</article>
+{{ end }}
diff --git a/layouts/robots.txt b/layouts/robots.txt
new file mode 100644
index 0000000..8fc0c20
--- /dev/null
+++ b/layouts/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Disallow: /search-index.json
+Disallow: /it/search-index.json
+
+Sitemap: {{ .Site.BaseURL }}sitemap.xml
diff --git a/layouts/section.html b/layouts/section.html
deleted file mode 100644
index d817087..0000000
--- a/layouts/section.html
+++ /dev/null
@@ -1,40 +0,0 @@
-{{ define "main" }}
- {{ if eq .Type "articles" }}
- <!-- Articles Section with Timeline -->
- <div class="page-header">
- <h1>{{ .Title }}</h1>
- <div class="filter-buttons">
- <button class="filter-btn active" data-filter="all">All</button>
- <button class="filter-btn" data-filter="tech">Tech</button>
- <button class="filter-btn" data-filter="life">Life</button>
- <button class="filter-btn" data-filter="quote">Quote</button>
- <button class="filter-btn" data-filter="link">Link</button>
- <button class="filter-btn" data-filter="photo">Photo</button>
- </div>
- </div>
-
- {{ $featured := (.Pages.ByDate.Reverse) }}
- {{ if and $featured (index $featured 0).Params.featured }}
- {{ partial "featured-card.html" (dict "page" (index $featured 0)) }}
- {{ end }}
-
- <section class="timeline-section">
- <div class="timeline-line"></div>
- <div class="timeline-feed">
- {{ $pages := .Pages.ByDate.Reverse }}
- {{ range $index, $page := $pages }}
- {{ partial "timeline-item.html" (dict "page" $page "index" $index) }}
- {{ end }}
- </div>
- </section>
-
- {{ $filters := resources.Get "js/filters.js" | fingerprint }}
- <script defer src="{{ $filters.RelPermalink }}"></script>
-
- {{ else }}
- <!-- Static Section (is/about, is/contact, etc.) -->
- <div class="container-narrow" style="padding-top: 2rem;">
- {{ .Content }}
- </div>
- {{ end }}
-{{ end }}
diff --git a/layouts/shortcodes/actions.html b/layouts/shortcodes/actions.html
index 296a211..7badab3 100644
--- a/layouts/shortcodes/actions.html
+++ b/layouts/shortcodes/actions.html
@@ -1,7 +1,19 @@
-{{ $link := .Get "url" }}
-{{ $desc := .Get "desc" }}
-{{ $outclass := .Get "outclass" }}
-{{ $inclass := .Get "inclass" }}
-<ul class="actions{{ with $outclass }} {{ . }}{{ end }}">
- <li><a href="{{ $link }}" class="button{{ with $inclass }} {{ . }}{{ end }}">{{ $desc }}</a></li>
-</ul>
+{{- $url := .Get "url" -}}
+{{- $desc := .Get "desc" | default "Download" -}}
+{{- $outclass := .Get "outclass" | default "" -}}
+{{- $inclass := .Get "inclass" | default "" -}}
+
+{{- if $url -}}
+<div class="my-6 {{ $outclass }}">
+ <a
+ href="{{ $url }}"
+ class="inline-flex items-center gap-2 px-4 py-2 rounded border border-accent/30 text-accent font-medium hover:border-accent/50 hover:bg-accent/10 transition-colors {{ $inclass }}"
+ download
+ >
+ <i data-feather="download" class="w-4 h-4"></i>
+ {{ $desc }}
+ </a>
+</div>
+{{- else -}}
+ {{- errorf "actions shortcode: 'url' parameter is required" -}}
+{{- end -}}
diff --git a/layouts/shortcodes/contact.html b/layouts/shortcodes/contact.html
new file mode 100644
index 0000000..6c5748f
--- /dev/null
+++ b/layouts/shortcodes/contact.html
@@ -0,0 +1,65 @@
+<form id="contact-form" x-data="contactForm()" @submit.prevent="submitContactForm" class="space-y-6">
+ <!-- Name Field -->
+ <div>
+ <label for="name" class="block text-sm font-medium text-text mb-2">
+ {{ i18n "name" }}
+ </label>
+ <input
+ id="name"
+ type="text"
+ x-model="formData.name"
+ required
+ class="w-full px-4 py-2 bg-bg border border-border/50 rounded-lg text-text placeholder-text-dim focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition-colors"
+ :aria-busy="isSubmitting"
+ />
+ </div>
+
+ <!-- Email Field -->
+ <div>
+ <label for="email" class="block text-sm font-medium text-text mb-2">
+ {{ i18n "email" }}
+ </label>
+ <input
+ id="email"
+ type="email"
+ x-model="formData.email"
+ required
+ class="w-full px-4 py-2 bg-bg border border-border/50 rounded-lg text-text placeholder-text-dim focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition-colors"
+ :aria-busy="isSubmitting"
+ />
+ </div>
+
+ <!-- Message Field -->
+ <div>
+ <label for="message" class="block text-sm font-medium text-text mb-2">
+ {{ i18n "message" }}
+ </label>
+ <textarea
+ id="message"
+ x-model="formData.message"
+ rows="5"
+ required
+ class="w-full px-4 py-2 bg-bg border border-border/50 rounded-lg text-text placeholder-text-dim focus:outline-none focus:border-accent focus:ring-1 focus:ring-accent transition-colors resize-none"
+ :aria-busy="isSubmitting"
+ ></textarea>
+ </div>
+
+ <!-- Status Message -->
+ <div
+ x-show="statusMessage"
+ x-text="statusMessage"
+ :class="statusClass"
+ class="px-4 py-3 rounded-lg text-sm transition-all"
+ ></div>
+
+ <!-- Submit Button -->
+ <button
+ type="submit"
+ :disabled="isSubmitting"
+ class="w-full px-4 py-2 bg-accent text-bg font-medium rounded-lg hover:bg-accent/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
+ >
+ <span x-show="!isSubmitting">{{ i18n "submit" }}</span>
+ <span x-show="isSubmitting">{{ i18n "sending" }}</span>
+ </button>
+</form>
+
diff --git a/layouts/shortcodes/div-close.html b/layouts/shortcodes/div-close.html
deleted file mode 100644
index ea7e47c..0000000
--- a/layouts/shortcodes/div-close.html
+++ /dev/null
@@ -1,2 +0,0 @@
-</div>
-
diff --git a/layouts/shortcodes/div.html b/layouts/shortcodes/div.html
deleted file mode 100644
index 0c0b755..0000000
--- a/layouts/shortcodes/div.html
+++ /dev/null
@@ -1,4 +0,0 @@
-{{ if .Get "class" }}
-<div class="{{ with .Get "class" }}{{ . }}{{ end }}">
-{{ end }}
-
diff --git a/layouts/shortcodes/dropcap.html b/layouts/shortcodes/dropcap.html
index 80cbefe..6114678 100644
--- a/layouts/shortcodes/dropcap.html
+++ b/layouts/shortcodes/dropcap.html
@@ -1,14 +1 @@
-{{/*
- * The dropcap shortcode:
- *
- * Usage:
- *
- * {{< dropcap class="some class" >}}
- * your paragraph that will have a drop cap here
- * {{< /dropcap >}}
- *
- */}}
-
-<p class="has-dropcap {{ with .Get "class"}}{{.}}{{ end }}">
- {{ .InnerDeindent }}
-</p>
+<span class="text-2xl font-bold first-letter:text-3xl">{{ .Inner }}</span>
diff --git a/layouts/shortcodes/em.html b/layouts/shortcodes/em.html
index 11f399a..a3a096c 100644
--- a/layouts/shortcodes/em.html
+++ b/layouts/shortcodes/em.html
@@ -1 +1 @@
-<mark>{{ .Inner | markdownify }}</mark>
+<em>{{ .Inner }}</em>
diff --git a/layouts/shortcodes/figure.html b/layouts/shortcodes/figure.html
deleted file mode 100644
index 4cf2e7b..0000000
--- a/layouts/shortcodes/figure.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{{/*
- * This version of the figure shortcode needs to be closed and doesn't
- * add an image itself, but relies on the img.html shortcode to provide
- * a responsive image instead.
- */}}
-<figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
- {{- if .Get "link" -}}
- <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
- {{- end -}}
- {{.InnerDeindent}}
- {{- if .Get "link" }}</a>{{ end -}}
- {{- if or (or (.Get "title") (.Get "caption")) (.Get "attr") -}}
- <figcaption>
- {{ with (.Get "title") -}}
- <h4>{{ . }}</h4>
- {{- end -}}
- {{- if or (.Get "caption") (.Get "attr") -}}<p>
- {{- .Get "caption" | markdownify -}}
- {{- with .Get "attrlink" }}
- <a href="{{ . }}">
- {{- end -}}
- {{- .Get "attr" | markdownify -}}
- {{- if .Get "attrlink" }}</a>{{ end }}</p>
- {{- end }}
- </figcaption>
- {{- end }}
-</figure>
diff --git a/layouts/shortcodes/gal-img.html b/layouts/shortcodes/gal-img.html
deleted file mode 100644
index ae7d23e..0000000
--- a/layouts/shortcodes/gal-img.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{{- $src := .Get "src" -}}
-{{- $source := resources.Get $src -}}
-{{- $alt := .Get "alt" -}}
-{{- $caption := .Get "caption" -}}
-{{- $location := .Get "location" -}}
-{{- $fullsize := $source -}}
-{{- $thumb := $source.Resize "400x webp" -}}
-<figure class="photo-card"
- data-photo-index="{{ .Ordinal }}"
- data-src="{{ $fullsize.RelPermalink }}"
- data-alt="{{ $alt }}"
- data-caption="{{ $caption }}"
- data-location="{{ $location }}">
- <img src="{{ $thumb.RelPermalink }}" alt="{{ $alt }}" loading="lazy">
- {{ with $caption }}<figcaption>{{ . }}</figcaption>{{ end }}
-</figure>
diff --git a/layouts/shortcodes/gallery.html b/layouts/shortcodes/gallery.html
index f9e546e..b66c327 100644
--- a/layouts/shortcodes/gallery.html
+++ b/layouts/shortcodes/gallery.html
@@ -1,3 +1,12 @@
-<div class="photo-grid" data-lightbox="true">
- {{ .Inner }}
+{{- $cols := .Get "cols" | default "2" -}}
+
+<div class="my-8 grid gap-4" style="grid-template-columns: repeat({{ $cols }}, 1fr)">
+ {{- range $line := strings.Split .Inner "\n" -}}
+ {{- if strings.Contains $line "![" -}}
+ {{- $image := strings.TrimSpace $line -}}
+ {{- if $image -}}
+ {{ $image | markdownify | safeHTML }}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
</div>
diff --git a/layouts/shortcodes/gravatar.html b/layouts/shortcodes/gravatar.html
index 56e2514..3a1ebcc 100644
--- a/layouts/shortcodes/gravatar.html
+++ b/layouts/shortcodes/gravatar.html
@@ -1,52 +1,17 @@
-{{/*
- * The gravatar shortcode:
- * All arguments are optional, main ones are mail and size and have a fallback set in place.
- * Args:
- * mail: [string] The email address. Falls back to .Site.Params.author_email which should be set in your config file.
- * size: [int] The size of the fetched image. Defaults to 200 if not set.
- * class: [string] The class to give to the figure block.
- * link: [string] The address to link the picture to.
- * target: [string] Where to open the link. One of "_blank", "_self", "_parent", "_top".
- * caption: [string] Caption text to show with the image. Supports Markdown.
- *
- * Usage:
- * {{< gravatar mail="some@address.com" size=150 class="some class" link="https://example.com" target="_blank" rel="author" caption="Here's a picture of a dog." >}}
- *
- * Output:
- * <figure class="some class">
- * <a href="https://example.com" target="_blank" rel="author">
- * <img src="https://www.gravatar.com/avatar/emailhash?s=150" alt="Here's a picture of a dog." />
- * </a>
- * <figcaption>
- * <p>
- * Here's a picture of a dog.
- * </p>
- * </figcaption>
- * </figure>
- *
- */}}
+{{- $email := .Get "email" -}}
+{{- $size := .Get "size" | default "256" | int -}}
+{{- $alt := .Get "alt" | default "User avatar" -}}
+{{- $class := .Get "class" | default "w-32 h-32 rounded-full" -}}
-{{- $mailhash := $.Site.Params.author_email -}}
-{{- if .Get "mail" -}}{{- $mailhash = .Get "mail" -}}{{- end -}}
-{{- $hash := $mailhash | lower | md5 -}}
-
-<figure{{ with .Get "class" }} class="{{ . }}"{{ end }}>
-{{- if .Get "link" -}}
- <a href="{{ .Get "link" }}"{{ with .Get "target" }} target="{{ . }}"{{ end }}{{ with .Get "rel" }} rel="{{ . }}"{{ end }}>
-{{- end }}
- <img src="https://www.gravatar.com/avatar/{{- $hash -}}?s={{- with .Get "size" }}{{.}}{{ else }}200{{ end }}"
- {{- if or (.Get "alt") (.Get "caption") }}
- alt="{{ with .Get "alt" }}{{ . }}{{ else }}{{ .Get "caption" | markdownify| plainify }}{{ end }}"
- {{- end -}}
- />
-{{- if .Get "link" -}}
- </a>
-{{- end }}
-{{- if .Get "caption" -}}
- <figcaption>
- <p>
- {{- .Get "caption" | markdownify -}}
- </p>
- </figcaption>
-{{- end }}
-</figure>
+{{- if $email -}}
+ {{- $hash := md5 (strings.TrimSpace (strings.ToLower $email)) -}}
+ {{- $gravatarURL := printf "https://www.gravatar.com/avatar/%s?s=%d&d=identicon" $hash $size -}}
+ <img
+ src="{{ $gravatarURL }}"
+ alt="{{ $alt }}"
+ class="{{ $class }}"
+ loading="lazy"
+ />
+{{- else -}}
+ {{- errorf "gravatar shortcode: 'email' parameter is required" -}}
+{{- end -}}
diff --git a/layouts/shortcodes/image.html b/layouts/shortcodes/image.html
new file mode 100644
index 0000000..84dec3f
--- /dev/null
+++ b/layouts/shortcodes/image.html
@@ -0,0 +1,23 @@
+{{- $src := .Get "src" -}}
+{{- $alt := .Get "alt" | default "Image" -}}
+{{- $caption := .Get "caption" -}}
+{{- $class := .Get "class" | default "w-full h-auto rounded-lg border border-border" -}}
+{{- $link := .Get "link" -}}
+{{- $figureClass := .Get "figure-class" -}}
+
+{{- if or $src .Inner -}}
+ <figure class="my-8{{- with $figureClass }} {{ . }}{{- end -}}">
+ {{- if .Inner -}}
+ {{- .Inner -}}
+ {{- else -}}
+ {{- if $link -}}<a href="{{ $link }}">{{- end -}}
+ <img src="{{ $src }}" alt="{{ $alt }}" class="{{ $class }}" loading="lazy" />
+ {{- if $link -}}</a>{{- end -}}
+ {{- end -}}
+ {{- if $caption -}}
+ <figcaption class="mt-3 text-center text-sm text-text-dim italic">{{ $caption }}</figcaption>
+ {{- end -}}
+ </figure>
+{{- else -}}
+ {{- errorf "image shortcode: 'src' parameter or inner content is required" -}}
+{{- end -}}
diff --git a/layouts/shortcodes/img.html b/layouts/shortcodes/img.html
deleted file mode 100644
index fbf96c0..0000000
--- a/layouts/shortcodes/img.html
+++ /dev/null
@@ -1,84 +0,0 @@
-{{/*
- Taken from https://www.brycewray.com/posts/2022/06/responsive-optimized-images-hugo/
-*/}}
-
-{{- $respSizes := .Site.Params.imageSizes -}}
-{{- $src := .Get "src" -}}
-{{- $source := resources.Get $src -}}
-{{- $alt := .Get "alt" -}}
-{{- $divClass := .Get "divClass" -}}
-{{/*
- The styling in $imgClass, below, makes
- an image fill the container horizontally
- and adjust its height automatically
- for that, and then fade in for the LQIP effect.
- Feel free to adjust your CSS/SCSS as desired.
-*/}}
-{{- $imgClass := "animate-fade" -}}
-{{- $dataSzes := "(min-width: 1024px) 100vw, 50vw" -}}
-{{/*
- Now we'll create the 20-pixel-wide LQIP
- and turn it into Base64-encoded data, which
- is better for performance and caching.
-*/}}
-{{- $LQIP_img := $source.Resize "20x jpg" -}}
-{{- $LQIP_b64 := $LQIP_img.Content | base64Encode -}}
-{{/*
- $CFPstyle is for use in styling
- the div's background, as you'll see shortly.
-*/}}
-{{- $CFPstyle := printf "%s%s%s%v%s" "background: url(data:image/jpeg;base64," $LQIP_b64 "); background-size: cover; background-repeat: no-repeat; width: " $source.Width "px;" -}}
-{{/*
- Then, we create a 640-pixel-wide JPG
- of the image. This will serve as the
- "fallback" image for that tiny percentage
- of browsers that don't understand the
- HTML `picture` tag.
-*/}}
-{{- $actualImg := $source.Resize "640x jpg" -}}
-<div class="picture{{with $divClass}} {{.}}{{end}}" style="{{ $CFPstyle | safeCSS }}">
-{{/*
- Now we'll build the `picture` which modern
- browsers use to decide which image, and
- which format thereof, to show. Remember to
- put `webp` first, since the browser will use
- the first format it **can** use, and WebP files
- usually are smaller. After WebP, the fallback
- is the universally safe JPG format.
-*/}}
- <a href="{{ $source.RelPermalink }}">
- <picture>
- <source
- type="image/webp"
- srcset="
- {{- with $respSizes -}}
- {{- range $i, $e := $respSizes -}}
- {{- if ge $source.Width . -}}
- {{- if $i }}, {{ end -}}{{- ($source.Resize (printf "%vx%v" $e " webp") ).RelPermalink }} {{ . }}w
- {{- end -}}
- {{- end -}}
- {{- end -}}"
- sizes="{{ $dataSzes }}"
- />
- <source
- type="image/jpeg"
- srcset="
- {{- with $respSizes -}}
- {{- range $i, $e := . -}}
- {{- if ge $source.Width . -}}
- {{- if $i }}, {{ end -}}{{- ($source.Resize (printf "%vx%v" . " jpg") ).RelPermalink }} {{ . }}w
- {{- end -}}
- {{- end -}}
- {{- end -}}"
- sizes="{{ $dataSzes }}"
- />
- <img class="{{ $imgClass }}"
- src="{{ $actualImg.RelPermalink }}"
- width="{{ $source.Width }}"
- height="{{ $source.Height }}"
- alt="{{ $alt }}"
- loading="lazy"
- />
- </picture>
- </a>
-</div>
diff --git a/layouts/shortcodes/quote.html b/layouts/shortcodes/quote.html
index 05d0c94..1ed5c3c 100644
--- a/layouts/shortcodes/quote.html
+++ b/layouts/shortcodes/quote.html
@@ -1,19 +1,15 @@
-<blockquote class="blockquote">
- <p>
- {{.Inner}}
- </p>
- {{ if .Get "source" }}
- <footer class="blockquote-footer">
- <cite title="by: {{ with .Get "source"}}{{.}}{{ end }}">
- {{ with .Get "src"}}
- by
- <a href="{{.}}" target="_blank" rel="noopener noreferrer">
- {{ end }}
- {{ with .Get "source" }}{{.}}{{ end }}
- {{ with .Get "src"}}
- </a>
- {{ end }}
- </cite>
- </footer>
- {{ end }}
+{{- $source := .Get "source" -}}
+{{- $src := .Get "src" -}}
+
+<blockquote class="my-8 pl-6 border-l-4 border-accent/50 italic text-text-dim">
+ <p class="text-lg">{{ .Inner }}</p>
+ {{- if $source -}}
+ <footer class="mt-4 text-sm not-italic text-text-dim">
+ {{- if $src -}}
+ — <a href="{{ $src }}" class="text-accent hover:underline">{{ $source }}</a>
+ {{- else -}}
+ — {{ $source }}
+ {{- end -}}
+ </footer>
+ {{- end -}}
</blockquote>
diff --git a/layouts/shortcodes/strike.html b/layouts/shortcodes/strike.html
index 415036a..275b7c1 100644
--- a/layouts/shortcodes/strike.html
+++ b/layouts/shortcodes/strike.html
@@ -1 +1 @@
-<strike>{{ .Inner | markdownify }}</strike>
+<s>{{ .Inner }}</s>
diff --git a/layouts/shortcodes/svg.html b/layouts/shortcodes/svg.html
deleted file mode 100644
index d042bb0..0000000
--- a/layouts/shortcodes/svg.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<span class="inline-svg{{ with .Get "class" }} {{ . }}{{ end }}">
-{{- $fname := print "SVGs/" ( .Get "name" ) ".svg" -}}
-{{ $icon := resources.Get $fname }}
-{{ $icon.Content | safeHTML }}
-</span>
diff --git a/layouts/shortcodes/video.html b/layouts/shortcodes/video.html
index aa30a13..1e2645d 100644
--- a/layouts/shortcodes/video.html
+++ b/layouts/shortcodes/video.html
@@ -1,37 +1,37 @@
-{{/*
- * The video shortcode:
- * All arguments are optional, except for src which is where you define your video file
- * This shortcode supports webm, mp4, and other HTML5 video formats.
- * Args:
- * src: [string] Path to video file (required)
- * class: [string] The class(es) to give to the video block.
- * width: [int] The width of the video
- * height: [int] The height of the video
- * autoplay: [bool] true or false for autoplay - defaults to false
- * loop: [bool] true or false for loop - defaults to false
- * muted: [bool] true or false for mute - defaults to false
- *
- * Usage:
- * {{< video src="my-video.mp4" width=600 height=400 autoplay=true loop=true muted=true class="responsive-video" >}}
- *
- * Output:
- * <video class="responsive-video" controls preload="auto" width="600" height="400" autoplay loop muted playsinline>
- * <source src="my-video.mp4" type="video/mp4">
- * </video>
- *
- */}}
+{{- $src := .Get "src" -}}
+{{- $id := .Get "id" -}}
+{{- $title := .Get "title" | default "Video" -}}
+{{- $class := .Get "class" | default "" -}}
-{{ $ext := (.Get "src") | path.Ext }}
-{{ $filetype := slicestr $ext 1}}
-
-<video{{ with .Get "class" }} class="{{ . }}"{{ end }}
- controls
- preload="auto"
- {{ with .Get "width" }}width="{{.}}"{{ end }}
- {{ with .Get "height" }}height="{{.}}"{{ end }}
- {{ if eq (.Get "autoplay") "true" }}autoplay {{ end }}
- {{ if eq (.Get "loop") "true" }}loop {{ end }}
- {{ if eq (.Get "muted") "true" }}muted {{ end }}
- playsinline >
- <source src="{{ ( .Get "src" ) }}" type="video/{{ $filetype }}">
-</video>
+{{- if $id -}}
+ <div class="my-6{{- with $class }} {{ . }}{{- end -}}">
+ <iframe
+ class="w-full aspect-video"
+ src="https://www.youtube.com/embed/{{ $id }}"
+ title="{{ $title }}"
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
+ allowfullscreen
+ ></iframe>
+ </div>
+{{- else if $src -}}
+ {{- $ext := $src | path.Ext -}}
+ {{- $filetype := slicestr $ext 1 -}}
+ {{- $videoURL := $src -}}
+ {{- $resource := .Page.Resources.GetMatch $src -}}
+ {{- if $resource -}}{{- $videoURL = $resource.RelPermalink -}}{{- end -}}
+ <video
+ {{- with $class }} class="{{ . }}"{{ end }}
+ controls
+ preload="auto"
+ playsinline
+ {{- with .Get "width" }} width="{{ . }}"{{ end }}
+ {{- with .Get "height" }} height="{{ . }}"{{ end }}
+ {{- if eq (.Get "autoplay") "true" }} autoplay{{ end }}
+ {{- if eq (.Get "loop") "true" }} loop{{ end }}
+ {{- if eq (.Get "muted") "true" }} muted{{ end }}
+ >
+ <source src="{{ $videoURL }}" type="video/{{ $filetype }}">
+ </video>
+{{- else -}}
+ {{- errorf "video shortcode: either 'src' or 'id' parameter is required" -}}
+{{- end -}}
diff --git a/layouts/taxonomy.html b/layouts/taxonomy.html
deleted file mode 100644
index c2e7875..0000000
--- a/layouts/taxonomy.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{{ define "main" }}
- <h1>{{ .Title }}</h1>
- {{ .Content }}
- {{ range .Pages }}
- <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
- {{ end }}
-{{ end }}
diff --git a/layouts/taxonomy/list.html b/layouts/taxonomy/list.html
new file mode 100644
index 0000000..c3a4317
--- /dev/null
+++ b/layouts/taxonomy/list.html
@@ -0,0 +1,49 @@
+{{ define "main" }}
+<div class="mx-auto px-4 py-12 max-w-4xl">
+ <!-- Page title with taxonomy name -->
+ <h1 class="text-4xl md:text-5xl font-bold text-accent mb-12">
+ {{ i18n .Data.Plural }}
+ </h1>
+
+ <!-- Articles list -->
+ <div class="space-y-6 max-w-3xl">
+ {{ $pinned := where .Pages "Params.pinned" true }}
+ {{ $unpinned := where .Pages "Params.pinned" false }}
+
+ <!-- If no unpinned posts found (because param is undefined), get all non-pinned pages -->
+ {{ if eq (len $unpinned) 0 }}
+ {{ $unpinned = where .Pages "Params.pinned" nil }}
+ {{ if eq (len $unpinned) 0 }}
+ {{ $unpinned = where .Pages "Params.pinned" "" }}
+ {{ end }}
+ {{ end }}
+
+ <!-- Pinned posts first -->
+ {{ range (sort $pinned "Date" "desc") }}
+ <div class="relative">
+ {{ partial "article-card.html" . }}
+ <span class="absolute top-4 right-4 px-2 py-0.5 rounded text-xs font-mono bg-surface text-text-dim border border-border/30">
+ {{ i18n "postCount" .Pages.Len }}
+ </span>
+ </div>
+ {{ end }}
+
+ <!-- Regular posts -->
+ {{ range (sort $unpinned "Date" "desc") }}
+ <div class="relative">
+ {{ partial "article-card.html" . }}
+ <span class="absolute top-4 right-4 px-2 py-0.5 rounded text-xs font-mono bg-surface text-text-dim border border-border/30">
+ {{ i18n "postCount" .Pages.Len }}
+ </span>
+ </div>
+ {{ end }}
+
+ <!-- Empty state -->
+ {{ if eq (len .Pages) 0 }}
+ <div class="py-12 text-center text-text-dim">
+ {{ i18n "noRelated" }}
+ </div>
+ {{ end }}
+ </div>
+</div>
+{{ end }}
diff --git a/layouts/taxonomy/term.html b/layouts/taxonomy/term.html
new file mode 100644
index 0000000..1b953bc
--- /dev/null
+++ b/layouts/taxonomy/term.html
@@ -0,0 +1,46 @@
+{{ define "main" }}
+<div class="mx-auto px-4 py-12 max-w-4xl">
+ <!-- Page title with taxonomy type -->
+ <h1 class="text-4xl md:text-5xl font-bold text-accent mb-2">
+ {{ i18n .Data.Singular }}
+ </h1>
+
+ <!-- Current term heading -->
+ <p class="text-lg text-text-dim mb-12">
+ <span class="inline-flex items-center px-3 py-1 rounded text-sm font-medium border border-accent/30 text-accent">
+ {{ .Title }}
+ </span>
+ </p>
+
+ <!-- Articles list -->
+ <div class="space-y-6 max-w-3xl">
+ {{ $pinned := where .Pages "Params.pinned" true }}
+ {{ $unpinned := where .Pages "Params.pinned" false }}
+
+ <!-- If no unpinned posts found (because param is undefined), get all non-pinned pages -->
+ {{ if eq (len $unpinned) 0 }}
+ {{ $unpinned = where .Pages "Params.pinned" nil }}
+ {{ if eq (len $unpinned) 0 }}
+ {{ $unpinned = where .Pages "Params.pinned" "" }}
+ {{ end }}
+ {{ end }}
+
+ <!-- Pinned posts first -->
+ {{ range (sort $pinned "Date" "desc") }}
+ {{ partial "article-card.html" . }}
+ {{ end }}
+
+ <!-- Regular posts -->
+ {{ range (sort $unpinned "Date" "desc") }}
+ {{ partial "article-card.html" . }}
+ {{ end }}
+
+ <!-- Empty state -->
+ {{ if eq (len .Pages) 0 }}
+ <div class="py-12 text-center text-text-dim">
+ {{ i18n "noRelated" }}
+ </div>
+ {{ end }}
+ </div>
+</div>
+{{ end }}
diff --git a/layouts/term.html b/layouts/term.html
deleted file mode 100644
index c2e7875..0000000
--- a/layouts/term.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{{ define "main" }}
- <h1>{{ .Title }}</h1>
- {{ .Content }}
- {{ range .Pages }}
- <h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
- {{ end }}
-{{ end }}
diff --git a/static/favicon.ico b/static/favicon.ico
deleted file mode 100644
index 67f8b77..0000000
--- a/static/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/theme.toml b/theme.toml
new file mode 100644
index 0000000..bcb8f6f
--- /dev/null
+++ b/theme.toml
@@ -0,0 +1,37 @@
+name = "danix.xyz Hacker"
+license = "MIT"
+licenselink = "https://opensource.org/licenses/MIT"
+description = "Bilingual portfolio/blog theme with hacker/open-source aesthetic, responsive sidebar, 5 article types, and configuration-driven structure."
+homepage = "https://github.com/danix/danix-xyz-hacker-theme"
+demosite = "https://danix.xyz"
+author = "Danilo Macrì"
+authorlink = "https://danix.xyz"
+version = "1.0.0"
+
+[params]
+ minVersion = "0.100.0"
+
+[[minfeaturesVersion]]
+ description = "Hugo extended required for Tailwind CSS via Pipes"
+ version = "0.100.0"
+
+[[features]]
+ name = "Bilingual (i18n)"
+
+[[features]]
+ name = "Responsive Design"
+
+[[features]]
+ name = "Dark/Light Theme Toggle"
+
+[[features]]
+ name = "Article Types (5)"
+
+[[features]]
+ name = "Shortcode System"
+
+[[features]]
+ name = "Alpine.js Interactions"
+
+[[exampleSiteRepo]]
+ url = "https://github.com/danix/danix.xyz"