2026-04-08 19:49:15 +02:00
|
|
|
{{- define "page-background" -}}
|
|
|
|
|
{{- with .Params.background -}}
|
|
|
|
|
<div class="fixed inset-0 -z-10 overflow-hidden" aria-hidden="true">
|
|
|
|
|
{{- partial "media.html" (dict "media" . "class" "w-full h-full object-cover opacity-[0.15] blur-md scale-110" "lazy" false) -}}
|
|
|
|
|
<div class="absolute inset-0" style="background: rgba(5,5,16,0.7)"></div>
|
|
|
|
|
</div>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
{{- define "main" -}}
|
|
|
|
|
|
|
|
|
|
<!-- ── ARTICLE HEADER ────────────────────────────────────────── -->
|
|
|
|
|
<header class="gutter-x pt-36 pb-12 max-w-5xl mx-auto relative overflow-hidden">
|
|
|
|
|
|
|
|
|
|
<!-- Faded decorative category text -->
|
|
|
|
|
{{- with index .Params.categories 0 -}}
|
|
|
|
|
<div class="graffiti-tag absolute -top-2 -right-4 select-none pointer-events-none"
|
|
|
|
|
style="-webkit-text-stroke-color: rgba(155,0,255,0.08);" aria-hidden="true">{{ . }}</div>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- Categories -->
|
|
|
|
|
{{- with .Params.categories -}}
|
|
|
|
|
<div class="flex flex-wrap gap-2 mb-6">
|
|
|
|
|
{{- range . -}}
|
|
|
|
|
<a
|
|
|
|
|
href="/categories/{{ . | urlize }}/"
|
|
|
|
|
class="badge badge-outline hover:bg-heat hover:text-white hover:border-heat transition-all"
|
|
|
|
|
>{{ . }}</a>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</div>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- Title -->
|
|
|
|
|
<h1 class="font-display text-5xl md:text-7xl lg:text-8xl text-paper text-balance mb-8 leading-none">
|
|
|
|
|
{{- .Title -}}
|
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
|
|
<!-- Description -->
|
|
|
|
|
{{- with .Description -}}
|
|
|
|
|
<p class="text-chalk text-xl md:text-2xl max-w-2xl mb-10 leading-relaxed text-pretty font-body font-light">
|
|
|
|
|
{{ . }}
|
|
|
|
|
</p>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- Author byline -->
|
|
|
|
|
{{- with .Params.author -}}
|
|
|
|
|
{{- $authorPage := site.GetPage (printf "authors/%s" .) -}}
|
|
|
|
|
{{- with $authorPage -}}
|
|
|
|
|
{{- partial "author-card.html" . -}}
|
|
|
|
|
{{- end -}}
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- Date + reading info -->
|
|
|
|
|
<div class="flex flex-wrap items-center gap-6">
|
|
|
|
|
<time class="label text-fog" datetime="{{ .Date.Format "2006-01-02" }}">
|
|
|
|
|
{{- .Date.Format "January 2, 2006" -}}
|
|
|
|
|
</time>
|
|
|
|
|
{{- $imgCount := len (.Resources.Match "[0-9]*.png") -}}
|
|
|
|
|
{{- $itemCount := $imgCount -}}
|
|
|
|
|
{{- if and (eq $imgCount 0) .Params.items -}}{{- $itemCount = len .Params.items -}}{{- end -}}
|
|
|
|
|
{{- if gt $itemCount 0 -}}
|
|
|
|
|
<span class="label text-fog">{{ $itemCount }} {{ if eq $itemCount 1 }}Work{{ else }}Works{{ end }}</span>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- ── GRADIENT DIVIDER ──────────────────────────────────────── -->
|
|
|
|
|
<div class="gradient-line mb-0"></div>
|
|
|
|
|
|
|
|
|
|
<!-- ── BANNER MEDIA — bundle banner.png → 01.png → .Params.banner ── -->
|
|
|
|
|
{{- $bannerImg := .Resources.GetMatch "banner.png" -}}
|
|
|
|
|
{{- if not $bannerImg -}}{{- $bannerImg = .Resources.GetMatch "01.png" -}}{{- end -}}
|
|
|
|
|
{{- if $bannerImg -}}
|
2026-04-14 15:17:20 +02:00
|
|
|
<div class="w-full overflow-hidden mb-16 md:mb-24" style="height: clamp(320px, 60vh, 75vh)">
|
|
|
|
|
{{- partial "img.html" (dict "res" $bannerImg "widths" (slice 1200 1800) "sizes" "100vw" "class" "w-full h-full object-cover object-center" "alt" .Title "loading" "eager") -}}
|
2026-04-08 19:49:15 +02:00
|
|
|
</div>
|
|
|
|
|
{{- else -}}{{- with .Params.banner -}}
|
2026-04-14 15:17:20 +02:00
|
|
|
<div class="w-full overflow-hidden mb-16 md:mb-24" style="height: clamp(320px, 60vh, 75vh)">
|
|
|
|
|
{{- partial "media.html" (dict "media" . "class" "w-full h-full object-cover object-center" "lazy" false) -}}
|
2026-04-08 19:49:15 +02:00
|
|
|
</div>
|
|
|
|
|
{{- end -}}{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- ── PROSE BODY ────────────────────────────────────────────── -->
|
|
|
|
|
{{- with .Content -}}
|
|
|
|
|
<article class="gutter-x mb-20 md:mb-28">
|
|
|
|
|
<div class="max-w-5xl mx-auto">
|
|
|
|
|
<div class="max-w-3xl prose-editorial">
|
|
|
|
|
{{ . }}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- ── GALLERY ───────────────────────────────────────────────── -->
|
|
|
|
|
{{- $bundleImages := .Resources.Match "[0-9]*.png" -}}
|
|
|
|
|
{{- $legacyItems := slice -}}
|
|
|
|
|
{{- with .Params.items -}}{{- $legacyItems = . -}}{{- end -}}
|
|
|
|
|
{{- if or $bundleImages $legacyItems -}}
|
|
|
|
|
<section class="gutter-x pb-24 md:pb-32" aria-label="Gallery">
|
|
|
|
|
<div class="max-w-5xl mx-auto">
|
|
|
|
|
|
|
|
|
|
{{- if $bundleImages -}}
|
2026-04-11 18:24:33 +02:00
|
|
|
{{- /* Pre-compute lightbox items so Alpine gets a static JSON array */ -}}
|
|
|
|
|
{{- $lbItems := slice -}}
|
|
|
|
|
{{- range $bundleImages -}}
|
2026-04-08 19:49:15 +02:00
|
|
|
{{- $stem := .Name | strings.TrimSuffix ".png" -}}
|
|
|
|
|
{{- $video := $.Resources.GetMatch (printf "%s.mp4" $stem) -}}
|
2026-04-11 18:24:33 +02:00
|
|
|
{{- $webp := .Resize "1200x webp" -}}
|
|
|
|
|
{{- $entry := dict "img" $webp.RelPermalink "video" "" -}}
|
|
|
|
|
{{- if $video -}}{{- $entry = dict "img" $webp.RelPermalink "video" $video.RelPermalink -}}{{- end -}}
|
|
|
|
|
{{- $lbItems = $lbItems | append $entry -}}
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<div
|
|
|
|
|
x-data="{
|
|
|
|
|
open: false,
|
|
|
|
|
idx: 0,
|
2026-04-11 18:33:45 +02:00
|
|
|
fill: false,
|
2026-04-11 18:24:33 +02:00
|
|
|
items: {{ $lbItems | jsonify }},
|
|
|
|
|
show(i) { this.idx = i; this.open = true },
|
2026-04-11 18:33:45 +02:00
|
|
|
close() { this.open = false; this.fill = false },
|
2026-04-11 18:24:33 +02:00
|
|
|
prev() { this.idx = (this.idx - 1 + this.items.length) % this.items.length },
|
|
|
|
|
next() { this.idx = (this.idx + 1) % this.items.length }
|
|
|
|
|
}"
|
|
|
|
|
@keydown.escape.window="open && close()"
|
|
|
|
|
@keydown.arrow-left.window="open && prev()"
|
|
|
|
|
@keydown.arrow-right.window="open && next()"
|
2026-04-11 18:33:45 +02:00
|
|
|
@keydown.f.window="open && (fill = !fill)"
|
2026-04-11 18:24:33 +02:00
|
|
|
>
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-5 mb-10">
|
|
|
|
|
<span class="badge badge-gradient">Gallery</span>
|
|
|
|
|
<span class="label text-fog">{{ len $bundleImages }} {{ if eq (len $bundleImages) 1 }}work{{ else }}works{{ end }}</span>
|
|
|
|
|
<div class="flex-1 gradient-line ml-2"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Numbered png+mp4 pairs from page bundle -->
|
|
|
|
|
<div class="columns-1 sm:columns-2 lg:columns-3 gap-5 space-y-5">
|
|
|
|
|
{{- range $i, $img := $bundleImages -}}
|
|
|
|
|
{{- $stem := $img.Name | strings.TrimSuffix ".png" -}}
|
|
|
|
|
{{- $video := $.Resources.GetMatch (printf "%s.mp4" $stem) -}}
|
|
|
|
|
<figure class="break-inside-avoid group card-comic cursor-zoom-in" @click="show({{ $i }})">
|
|
|
|
|
<div class="card-media min-h-48">
|
|
|
|
|
{{- if $video -}}
|
|
|
|
|
<video class="w-full pointer-events-none" src="{{ $video.RelPermalink }}" poster="{{ $img.RelPermalink }}" autoplay loop muted playsinline></video>
|
|
|
|
|
{{- else -}}
|
|
|
|
|
{{- partial "img.html" (dict "res" $img "widths" (slice 800 1200) "sizes" "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" "class" "w-full pointer-events-none") -}}
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</div>
|
|
|
|
|
</figure>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Lightbox — teleported to <body> to escape stacking context -->
|
|
|
|
|
<template x-teleport="body">
|
|
|
|
|
<div
|
|
|
|
|
x-show="open"
|
|
|
|
|
x-cloak
|
|
|
|
|
x-transition:enter="transition duration-200 ease-out"
|
|
|
|
|
x-transition:enter-start="opacity-0"
|
|
|
|
|
x-transition:enter-end="opacity-100"
|
|
|
|
|
x-transition:leave="transition duration-150 ease-in"
|
|
|
|
|
x-transition:leave-start="opacity-100"
|
|
|
|
|
x-transition:leave-end="opacity-0"
|
|
|
|
|
class="fixed inset-0 z-50 flex items-center justify-center bg-void/95 backdrop-blur-sm"
|
|
|
|
|
@click.self="close()"
|
|
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-label="Media lightbox"
|
|
|
|
|
>
|
2026-04-11 18:37:33 +02:00
|
|
|
<!-- ── Top chrome bar ── -->
|
|
|
|
|
<div class="absolute top-0 inset-x-0 z-10 flex items-center justify-between px-2 h-14"
|
|
|
|
|
style="background: linear-gradient(to bottom, rgba(5,5,16,0.85), transparent)">
|
|
|
|
|
<!-- gradient accent -->
|
|
|
|
|
<div class="absolute top-0 inset-x-0 h-px gradient-line"></div>
|
|
|
|
|
<!-- left spacer (mirrors right buttons width) -->
|
|
|
|
|
<div class="w-24"></div>
|
|
|
|
|
<!-- counter centred -->
|
|
|
|
|
<div class="label text-fog tabular-nums"
|
|
|
|
|
x-text="`${String(idx + 1).padStart(2,'0')} / ${String(items.length).padStart(2,'0')}`"></div>
|
|
|
|
|
<!-- right actions: fill toggle + close -->
|
|
|
|
|
<div class="flex items-center w-24 justify-end gap-1">
|
|
|
|
|
<button
|
|
|
|
|
@click="fill = !fill"
|
|
|
|
|
class="w-10 h-10 flex items-center justify-center label transition-colors"
|
|
|
|
|
:class="fill ? 'text-heat hover:text-chalk' : 'text-fog hover:text-heat'"
|
|
|
|
|
:aria-label="fill ? 'Switch to fit mode' : 'Switch to fill mode'"
|
|
|
|
|
x-text="fill ? 'FIT' : 'FILL'"
|
|
|
|
|
></button>
|
|
|
|
|
<button
|
|
|
|
|
@click="close()"
|
|
|
|
|
class="w-10 h-10 flex items-center justify-center label text-fog hover:text-chalk transition-colors"
|
|
|
|
|
aria-label="Close"
|
|
|
|
|
>✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-04-11 18:24:33 +02:00
|
|
|
|
2026-04-11 18:37:33 +02:00
|
|
|
<!-- ── Prev ── -->
|
2026-04-11 18:24:33 +02:00
|
|
|
<button
|
|
|
|
|
@click="prev()"
|
|
|
|
|
x-show="items.length > 1"
|
2026-04-11 18:37:33 +02:00
|
|
|
class="absolute left-2 top-1/2 -translate-y-1/2 z-10 w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-fog hover:text-heat transition-colors text-xl"
|
2026-04-11 18:24:33 +02:00
|
|
|
aria-label="Previous"
|
|
|
|
|
>{{ partial "icon.html" "arrow-left" }}</button>
|
|
|
|
|
|
2026-04-11 18:37:33 +02:00
|
|
|
<!-- ── Media ── -->
|
2026-04-11 18:33:45 +02:00
|
|
|
<div
|
2026-04-11 18:37:33 +02:00
|
|
|
class="absolute inset-0 flex items-center justify-center transition-[padding] duration-300"
|
|
|
|
|
:class="fill ? 'p-0' : 'pt-14 pb-14 px-14 md:px-20'"
|
2026-04-11 18:33:45 +02:00
|
|
|
>
|
2026-04-11 18:24:33 +02:00
|
|
|
<video
|
|
|
|
|
x-show="items[idx] && items[idx].video"
|
|
|
|
|
:src="items[idx] && items[idx].video ? items[idx].video : ''"
|
|
|
|
|
:poster="items[idx] ? items[idx].img : ''"
|
|
|
|
|
x-effect="if (open && items[idx] && items[idx].video) { $el.load(); $el.play() }"
|
2026-04-11 18:33:45 +02:00
|
|
|
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
|
|
|
|
class="transition-all duration-300"
|
2026-04-11 18:24:33 +02:00
|
|
|
controls loop muted playsinline
|
|
|
|
|
></video>
|
|
|
|
|
<img
|
|
|
|
|
x-show="items[idx] && !items[idx].video"
|
|
|
|
|
:src="items[idx] && !items[idx].video ? items[idx].img : ''"
|
|
|
|
|
alt=""
|
2026-04-11 18:33:45 +02:00
|
|
|
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
|
|
|
|
class="transition-all duration-300"
|
2026-04-11 18:24:33 +02:00
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-11 18:37:33 +02:00
|
|
|
<!-- ── Next ── -->
|
2026-04-11 18:24:33 +02:00
|
|
|
<button
|
|
|
|
|
@click="next()"
|
|
|
|
|
x-show="items.length > 1"
|
2026-04-11 18:37:33 +02:00
|
|
|
class="absolute right-2 top-1/2 -translate-y-1/2 z-10 w-10 h-10 md:w-12 md:h-12 flex items-center justify-center text-fog hover:text-heat transition-colors text-xl"
|
2026-04-11 18:24:33 +02:00
|
|
|
aria-label="Next"
|
|
|
|
|
>{{ partial "icon.html" "arrow-right" }}</button>
|
|
|
|
|
|
2026-04-11 18:37:33 +02:00
|
|
|
<!-- ── Bottom chrome bar (dots) ── -->
|
|
|
|
|
<div x-show="items.length > 1"
|
|
|
|
|
class="absolute bottom-0 inset-x-0 z-10 flex items-center justify-center h-14 gap-2"
|
|
|
|
|
style="background: linear-gradient(to top, rgba(5,5,16,0.85), transparent)">
|
|
|
|
|
<div class="absolute bottom-0 inset-x-0 h-px gradient-line"></div>
|
2026-04-11 18:24:33 +02:00
|
|
|
<template x-for="(item, i) in items" :key="i">
|
|
|
|
|
<button
|
|
|
|
|
@click="idx = i"
|
|
|
|
|
class="h-1.5 rounded-full transition-all duration-300"
|
|
|
|
|
:class="i === idx ? 'w-5 bg-heat' : 'w-1.5 bg-fog hover:bg-chalk'"
|
|
|
|
|
:aria-label="`Go to item ${i + 1}`"
|
|
|
|
|
></button>
|
|
|
|
|
</template>
|
|
|
|
|
</div>
|
2026-04-08 19:49:15 +02:00
|
|
|
</div>
|
2026-04-11 18:24:33 +02:00
|
|
|
</template>
|
|
|
|
|
|
2026-04-08 19:49:15 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{{- else -}}
|
|
|
|
|
<!-- Legacy front-matter items -->
|
|
|
|
|
<div class="flex items-center gap-5 mb-10">
|
|
|
|
|
<span class="badge badge-gradient">Gallery</span>
|
|
|
|
|
<span class="label text-fog">{{ len $legacyItems }} {{ if eq (len $legacyItems) 1 }}work{{ else }}works{{ end }}</span>
|
|
|
|
|
<div class="flex-1 gradient-line ml-2"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div x-data="{ selected: null }" class="columns-1 sm:columns-2 lg:columns-3 gap-5 space-y-5">
|
|
|
|
|
{{- range $i, $item := $legacyItems -}}
|
|
|
|
|
<figure class="break-inside-avoid group cursor-pointer card-comic" @click="selected = selected === {{ $i }} ? null : {{ $i }}">
|
|
|
|
|
<div class="card-media">
|
|
|
|
|
{{- partial "media.html" (dict "media" $item "class" "w-full") -}}
|
|
|
|
|
</div>
|
|
|
|
|
{{- with $item.description -}}
|
|
|
|
|
<figcaption class="px-4 py-3 text-fog text-sm leading-relaxed font-body transition-colors group-hover:text-chalk">{{ . }}</figcaption>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</figure>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</div>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- ── TAGS ─────────────────────────────────────────────────── -->
|
|
|
|
|
{{- with .Params.tags -}}
|
|
|
|
|
<footer class="gutter-x border-t border-zinc pt-10 pb-20">
|
|
|
|
|
<div class="max-w-5xl mx-auto flex flex-wrap gap-3 items-center">
|
|
|
|
|
<span class="label text-fog">Tagged:</span>
|
|
|
|
|
{{- range . -}}
|
|
|
|
|
<a
|
|
|
|
|
href="/tags/{{ . | urlize }}/"
|
|
|
|
|
class="badge badge-outline hover:bg-heat hover:text-white hover:border-heat transition-all"
|
|
|
|
|
># {{ . }}</a>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
<!-- ── RELATED POSTS ─────────────────────────────────────────── -->
|
|
|
|
|
{{- $related := .Site.RegularPages.Related . | first 3 -}}
|
|
|
|
|
{{- with $related -}}
|
2026-04-11 18:20:01 +02:00
|
|
|
<section class="bg-concrete border-t border-zinc gutter-x py-16 md:py-24 spray-bg">
|
2026-04-08 19:49:15 +02:00
|
|
|
<div class="max-w-5xl mx-auto">
|
|
|
|
|
<div class="flex items-center gap-5 mb-10">
|
|
|
|
|
<h2 class="font-display text-3xl md:text-4xl text-paper leading-none">
|
|
|
|
|
More <span class="text-gradient">Drops</span>
|
|
|
|
|
</h2>
|
|
|
|
|
<div class="flex-1 gradient-line ml-2"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
|
|
|
|
{{- range . -}}
|
|
|
|
|
{{- partial "post-card.html" . -}}
|
|
|
|
|
{{- end -}}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{{- end -}}
|
|
|
|
|
|
|
|
|
|
{{- end -}}
|