Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42e3ea6719 | ||
|
|
d3c4a06c82 | ||
|
|
63f29884db | ||
|
|
ad6b6b2834 | ||
|
|
bbfba94454 | ||
|
|
f2b2e2b79d | ||
|
|
71828c2379 | ||
|
|
e426798c42 | ||
|
|
a379b0d00f | ||
|
|
1aceeeae36 | ||
|
|
d129c545dd | ||
|
|
420c6466ca | ||
|
|
cf469081f8 | ||
|
|
8e6d8c52a2 | ||
|
|
194157c94c | ||
|
|
3d644c6da0 | ||
|
|
bfe97b9595 | ||
|
|
c8f9c10773 | ||
|
|
987c715c00 | ||
|
|
bc09fa2ad4 | ||
|
|
84bc1e8d82 | ||
|
|
847a7bc6d1 | ||
|
|
52c2b11fa9 | ||
|
|
55ad9c2dde | ||
|
|
1edb3fc140 | ||
|
|
dade82664e | ||
|
|
d4dcd61e88 | ||
|
|
67bbeba586 | ||
|
|
e66da58826 | ||
|
|
1bbdd33c0c | ||
|
|
ea2610d4a8 | ||
|
|
436223913e | ||
|
|
b78bc26322 |
@@ -0,0 +1,5 @@
|
|||||||
|
public/
|
||||||
|
resources/
|
||||||
|
node_modules/
|
||||||
|
.hugo_build.lock
|
||||||
|
static/
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-go-template", "prettier-plugin-toml"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["layouts/**/*.html", "themes/**/*.html"],
|
||||||
|
"options": {
|
||||||
|
"parser": "go-template",
|
||||||
|
"goTemplateBracketSpacing": true,
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.toml",
|
||||||
|
"options": {
|
||||||
|
"parser": "toml"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.css",
|
||||||
|
"options": {
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": "*.md",
|
||||||
|
"options": {
|
||||||
|
"proseWrap": "preserve"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
# Stage 1: Build
|
# Stage 1: Build
|
||||||
FROM node:22-alpine AS builder
|
# hugomods image bundles Hugo Extended + Node at matching, tested versions —
|
||||||
|
# Hugo's css.TailwindCSS execs node_modules/.bin/tailwindcss during the build,
|
||||||
|
# and pnpm-workspace.yaml's preferSymlinkedExecutables makes that a plain
|
||||||
|
# symlink Hugo's exec-security check accepts (see layouts/partials/head.html).
|
||||||
|
FROM hugomods/hugo:debian-node-0.165.0 AS builder
|
||||||
|
|
||||||
# Install Hugo + FFmpeg (video compression)
|
# Install FFmpeg (video compression)
|
||||||
RUN apk add --no-cache hugo ffmpeg git
|
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Install pnpm
|
# Install pnpm
|
||||||
RUN corepack enable && corepack prepare pnpm@latest --activate
|
RUN npm install -g pnpm
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files first (for layer caching)
|
# Copy package files first (for layer caching)
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
# Copy source files
|
# Copy source files
|
||||||
|
|||||||
@@ -5,55 +5,62 @@
|
|||||||
@source "../../content/**/*.md";
|
@source "../../content/**/*.md";
|
||||||
@source "../../assets/js/**/*.js";
|
@source "../../assets/js/**/*.js";
|
||||||
|
|
||||||
|
@custom-variant hover (&:hover);
|
||||||
|
|
||||||
/* ── Design tokens — sneaker drop culture ────────────────────── */
|
/* ── Design tokens — sneaker drop culture ────────────────────── */
|
||||||
@theme {
|
@theme {
|
||||||
|
|
||||||
/* Dark base — deep navy-black */
|
/* Dark base — deep navy-black */
|
||||||
--color-void: #050510;
|
--color-void: #050510;
|
||||||
--color-ink: #09091A;
|
--color-ink: #09091a;
|
||||||
--color-concrete: #0F0F22;
|
--color-concrete: #0f0f22;
|
||||||
--color-zinc: #1C1C38;
|
--color-zinc: #1c1c38;
|
||||||
--color-smoke: #383860;
|
--color-smoke: #383860;
|
||||||
--color-fog: #6868A0;
|
--color-fog: #6868a0;
|
||||||
--color-chalk: #B8B8E0;
|
--color-chalk: #b8b8e0;
|
||||||
--color-paper: #EEEEFF;
|
--color-paper: #eeeeff;
|
||||||
|
|
||||||
/* Gradient triad — pink → purple → ice */
|
/* Gradient triad — pink → purple → ice */
|
||||||
--color-heat: #FF1A8C; /* hot pink — primary energy */
|
--color-heat: #ff1a8c; /* hot pink — primary energy */
|
||||||
--color-pulse: #9B00FF; /* electric purple — mid */
|
--color-pulse: #9b00ff; /* electric purple — mid */
|
||||||
--color-frost: #00C8FF; /* ice blue — cool accent */
|
--color-frost: #00c8ff; /* ice blue — cool accent */
|
||||||
--color-ember: #FF4455; /* alert red */
|
--color-ember: #ff4455; /* alert red */
|
||||||
--color-lime: #1AFF8D; /* acid lime — highlight */
|
--color-lime: #1aff8d; /* acid lime — highlight */
|
||||||
--color-stem: #FF7A1A; /* peony stem orange */
|
--color-stem: #ff7a1a; /* peony stem orange */
|
||||||
|
|
||||||
/* Backward-compat aliases so existing templates still work */
|
/* Backward-compat aliases so existing templates still work */
|
||||||
--color-gold: var(--color-heat);
|
--color-gold: var(--color-heat);
|
||||||
--color-gold-dim: #CC1570;
|
--color-gold-dim: #cc1570;
|
||||||
--color-hot: var(--color-heat);
|
--color-hot: var(--color-heat);
|
||||||
--color-volt: var(--color-lime);
|
--color-volt: var(--color-lime);
|
||||||
--color-ice: var(--color-frost);
|
--color-ice: var(--color-frost);
|
||||||
--color-mist: var(--color-zinc);
|
--color-mist: var(--color-zinc);
|
||||||
--color-ash: var(--color-smoke);
|
--color-ash: var(--color-smoke);
|
||||||
--color-graphite: var(--color-concrete);
|
--color-graphite: var(--color-concrete);
|
||||||
--color-charcoal: var(--color-ink);
|
--color-charcoal: var(--color-ink);
|
||||||
|
|
||||||
/* ── Typography ─────────────────────────────────────────────── */
|
/* ── Typography ─────────────────────────────────────────────── */
|
||||||
--font-display: "Bebas Neue", "Arial Black", sans-serif;
|
--font-display: "Bebas Neue", "Arial Black", sans-serif;
|
||||||
--font-body: "Barlow", system-ui, sans-serif;
|
--font-body: "Barlow", system-ui, sans-serif;
|
||||||
--font-comic: "Unbounded", "Arial Black", sans-serif;
|
--font-comic: "Unbounded", "Arial Black", sans-serif;
|
||||||
--font-mono: "Share Tech Mono", "Courier New", monospace;
|
--font-mono: "Share Tech Mono", "Courier New", monospace;
|
||||||
|
|
||||||
/* ── Easing ─────────────────────────────────────────────────── */
|
/* ── Easing ─────────────────────────────────────────────────── */
|
||||||
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
--ease-sharp: cubic-bezier(0.4, 0, 0.6, 1);
|
--ease-sharp: cubic-bezier(0.4, 0, 0.6, 1);
|
||||||
--ease-cinematic: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
--ease-cinematic: cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Base ─────────────────────────────────────────────────────── */
|
/* ── Base ─────────────────────────────────────────────────────── */
|
||||||
@layer base {
|
@layer base {
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
*,
|
||||||
[x-cloak] { display: none !important; }
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--color-void);
|
background-color: var(--color-void);
|
||||||
@@ -66,9 +73,16 @@
|
|||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
body { min-height: 100vh; }
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
@@ -82,7 +96,12 @@
|
|||||||
transition: color 0.2s var(--ease-sharp);
|
transition: color 0.2s var(--ease-sharp);
|
||||||
}
|
}
|
||||||
|
|
||||||
img, video { max-width: 100%; height: auto; display: block; }
|
img,
|
||||||
|
video {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background-color: var(--color-heat);
|
background-color: var(--color-heat);
|
||||||
@@ -95,14 +114,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Gradient scrollbar */
|
/* Gradient scrollbar */
|
||||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
::-webkit-scrollbar {
|
||||||
::-webkit-scrollbar-track { background: var(--color-ink); }
|
width: 4px;
|
||||||
::-webkit-scrollbar-thumb { background: linear-gradient(180deg, var(--color-heat), var(--color-frost)); }
|
height: 4px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: var(--color-ink);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, var(--color-heat), var(--color-frost));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Components ───────────────────────────────────────────────── */
|
/* ── Components ───────────────────────────────────────────────── */
|
||||||
@layer components {
|
@layer components {
|
||||||
|
|
||||||
/* ── Gradient text ─────────────────────────────────────────── */
|
/* ── Gradient text ─────────────────────────────────────────── */
|
||||||
.text-gradient {
|
.text-gradient {
|
||||||
background: var(--color-heat);
|
background: var(--color-heat);
|
||||||
@@ -113,7 +138,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.text-gradient-animated {
|
.text-gradient-animated {
|
||||||
background: linear-gradient(90deg, var(--color-heat), var(--color-pulse), var(--color-frost), var(--color-heat));
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-heat),
|
||||||
|
var(--color-pulse),
|
||||||
|
var(--color-frost),
|
||||||
|
var(--color-heat)
|
||||||
|
);
|
||||||
background-size: 300% 100%;
|
background-size: 300% 100%;
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
@@ -128,7 +159,7 @@
|
|||||||
to top,
|
to top,
|
||||||
rgba(5, 5, 16, 0.98) 0%,
|
rgba(5, 5, 16, 0.98) 0%,
|
||||||
rgba(5, 5, 16, 0.65) 40%,
|
rgba(5, 5, 16, 0.65) 40%,
|
||||||
rgba(5, 5, 16, 0.1) 70%,
|
rgba(5, 5, 16, 0.1) 70%,
|
||||||
transparent 100%
|
transparent 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -136,8 +167,8 @@
|
|||||||
.overlay-gradient-hero {
|
.overlay-gradient-hero {
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
155deg,
|
155deg,
|
||||||
rgba(5, 5, 16, 0.1) 0%,
|
rgba(5, 5, 16, 0.1) 0%,
|
||||||
rgba(5, 5, 16, 0.5) 45%,
|
rgba(5, 5, 16, 0.5) 45%,
|
||||||
rgba(5, 5, 16, 0.97) 100%
|
rgba(5, 5, 16, 0.97) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -165,23 +196,58 @@
|
|||||||
padding: 5px 16px;
|
padding: 5px 16px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
|
clip-path: polygon(8px 0, 100% 0, calc(100% - 8px) 100%, 0 100%);
|
||||||
transition: transform 0.15s var(--ease-bounce), filter 0.2s;
|
transition:
|
||||||
|
transform 0.15s var(--ease-bounce),
|
||||||
|
filter 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.badge:hover { transform: skewX(-4deg) scale(1.06); filter: brightness(1.2); }
|
a.badge:hover {
|
||||||
a.badge, a > .badge { cursor: pointer; }
|
transform: skewX(-4deg) scale(1.06);
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
a.badge,
|
||||||
|
a > .badge {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.badge-heat { background: var(--color-heat); color: #fff; }
|
.badge-heat {
|
||||||
.badge-pulse { background: var(--color-pulse); color: #fff; }
|
background: var(--color-heat);
|
||||||
.badge-frost { background: var(--color-frost); color: var(--color-void); }
|
color: #fff;
|
||||||
.badge-lime { background: var(--color-lime); color: var(--color-void); }
|
}
|
||||||
.badge-ember { background: var(--color-ember); color: #fff; }
|
.badge-pulse {
|
||||||
.badge-stem { background: var(--color-stem); color: var(--color-void); }
|
background: var(--color-pulse);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.badge-frost {
|
||||||
|
background: var(--color-frost);
|
||||||
|
color: var(--color-void);
|
||||||
|
}
|
||||||
|
.badge-lime {
|
||||||
|
background: var(--color-lime);
|
||||||
|
color: var(--color-void);
|
||||||
|
}
|
||||||
|
.badge-ember {
|
||||||
|
background: var(--color-ember);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.badge-stem {
|
||||||
|
background: var(--color-stem);
|
||||||
|
color: var(--color-void);
|
||||||
|
}
|
||||||
|
|
||||||
/* Semantic aliases kept for template compat */
|
/* Semantic aliases kept for template compat */
|
||||||
.badge-hot { background: var(--color-heat); color: #fff; }
|
.badge-hot {
|
||||||
.badge-volt { background: var(--color-lime); color: var(--color-void); }
|
background: var(--color-heat);
|
||||||
.badge-ice { background: var(--color-frost); color: var(--color-void); }
|
color: #fff;
|
||||||
|
}
|
||||||
|
.badge-volt {
|
||||||
|
background: var(--color-lime);
|
||||||
|
color: var(--color-void);
|
||||||
|
}
|
||||||
|
.badge-ice {
|
||||||
|
background: var(--color-frost);
|
||||||
|
color: var(--color-void);
|
||||||
|
}
|
||||||
|
|
||||||
.badge-gradient {
|
.badge-gradient {
|
||||||
background: var(--color-heat);
|
background: var(--color-heat);
|
||||||
@@ -214,13 +280,13 @@
|
|||||||
clip-path: polygon(10px 0, 100% 0, calc(100% - 10px) 100%, 0 100%);
|
clip-path: polygon(10px 0, 100% 0, calc(100% - 10px) 100%, 0 100%);
|
||||||
transition:
|
transition:
|
||||||
box-shadow 0.25s var(--ease-sharp),
|
box-shadow 0.25s var(--ease-sharp),
|
||||||
transform 0.2s var(--ease-bounce);
|
transform 0.2s var(--ease-bounce);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* shimmer sweep */
|
/* shimmer sweep */
|
||||||
.btn::before {
|
.btn::before {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
@@ -236,11 +302,16 @@
|
|||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 8px 32px rgba(255, 26, 140, 0.5),
|
box-shadow:
|
||||||
0 2px 8px rgba(155, 0, 255, 0.2);
|
0 8px 32px rgba(255, 26, 140, 0.5),
|
||||||
|
0 2px 8px rgba(155, 0, 255, 0.2);
|
||||||
|
}
|
||||||
|
.btn:hover::before {
|
||||||
|
transform: translateX(200%);
|
||||||
|
}
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0px);
|
||||||
}
|
}
|
||||||
.btn:hover::before { transform: translateX(200%); }
|
|
||||||
.btn:active { transform: translateY(0px); }
|
|
||||||
|
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -248,7 +319,9 @@
|
|||||||
color: var(--color-heat);
|
color: var(--color-heat);
|
||||||
clip-path: none;
|
clip-path: none;
|
||||||
}
|
}
|
||||||
.btn-outline::before { display: none; }
|
.btn-outline::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.btn-outline:hover {
|
.btn-outline:hover {
|
||||||
background: var(--color-heat);
|
background: var(--color-heat);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@@ -269,7 +342,9 @@
|
|||||||
color: var(--color-chalk);
|
color: var(--color-chalk);
|
||||||
clip-path: none;
|
clip-path: none;
|
||||||
}
|
}
|
||||||
.btn-ghost::before { display: none; }
|
.btn-ghost::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
.btn-ghost:hover {
|
.btn-ghost:hover {
|
||||||
border-color: var(--color-heat);
|
border-color: var(--color-heat);
|
||||||
color: var(--color-heat);
|
color: var(--color-heat);
|
||||||
@@ -301,13 +376,13 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition:
|
transition:
|
||||||
border-color 0.2s var(--ease-sharp),
|
border-color 0.2s var(--ease-sharp),
|
||||||
box-shadow 0.3s var(--ease-out-expo),
|
box-shadow 0.3s var(--ease-out-expo),
|
||||||
transform 0.25s var(--ease-out-expo);
|
transform 0.25s var(--ease-out-expo);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* gradient top bar */
|
/* gradient top bar */
|
||||||
.card-comic::after {
|
.card-comic::after {
|
||||||
content: '';
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
@@ -325,25 +400,30 @@
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1.5px rgba(255, 26, 140, 0.4),
|
0 0 0 1.5px rgba(255, 26, 140, 0.4),
|
||||||
0 20px 60px rgba(255, 26, 140, 0.18),
|
0 20px 60px rgba(255, 26, 140, 0.18),
|
||||||
0 8px 24px rgba(155, 0, 255, 0.12),
|
0 8px 24px rgba(155, 0, 255, 0.12),
|
||||||
6px 6px 0 rgba(255, 26, 140, 0.22);
|
6px 6px 0 rgba(255, 26, 140, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-comic:hover::after { opacity: 1; }
|
.card-comic:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Isometric triangle grid — 3 sets of lines at 0° / 60° / -60°
|
/* ── Isometric triangle grid — 3 sets of lines at 0° / 60° / -60°
|
||||||
Creates an infinite isometric floor-tile illusion.
|
Creates an infinite isometric floor-tile illusion.
|
||||||
Colors: heat (horizontal) · frost (60°) · pulse (-60°)
|
Colors: heat (horizontal) · frost (60°) · pulse (-60°)
|
||||||
────────────────────────────────────────────────────────────── */
|
────────────────────────────────────────────────────────────── */
|
||||||
.speed-lines {
|
.speed-lines {
|
||||||
background-image:
|
background-image: repeating-linear-gradient(
|
||||||
repeating-linear-gradient(
|
-55deg,
|
||||||
-55deg,
|
rgba(255, 26, 140, 0.14) 0,
|
||||||
rgba(255, 26, 140, 0.14) 0, rgba(255, 26, 140, 0.14) 1px,
|
rgba(255, 26, 140, 0.14) 1px,
|
||||||
transparent 1px, transparent 22px,
|
transparent 1px,
|
||||||
rgba(0, 200, 255, 0.08) 22px, rgba(0, 200, 255, 0.08) 23px,
|
transparent 22px,
|
||||||
transparent 23px, transparent 44px
|
rgba(0, 200, 255, 0.08) 22px,
|
||||||
);
|
rgba(0, 200, 255, 0.08) 23px,
|
||||||
|
transparent 23px,
|
||||||
|
transparent 44px
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Diamond crosshatch ──────────────────────────────────── */
|
/* ── Diamond crosshatch ──────────────────────────────────── */
|
||||||
@@ -351,13 +431,17 @@
|
|||||||
background-image:
|
background-image:
|
||||||
repeating-linear-gradient(
|
repeating-linear-gradient(
|
||||||
45deg,
|
45deg,
|
||||||
rgba(255, 26, 140, 0.07) 0, rgba(255, 26, 140, 0.07) 1px,
|
rgba(255, 26, 140, 0.07) 0,
|
||||||
transparent 1px, transparent 24px
|
rgba(255, 26, 140, 0.07) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 24px
|
||||||
),
|
),
|
||||||
repeating-linear-gradient(
|
repeating-linear-gradient(
|
||||||
-45deg,
|
-45deg,
|
||||||
rgba(0, 200, 255, 0.05) 0, rgba(0, 200, 255, 0.05) 1px,
|
rgba(0, 200, 255, 0.05) 0,
|
||||||
transparent 1px, transparent 24px
|
rgba(0, 200, 255, 0.05) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 24px
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,9 +449,13 @@
|
|||||||
.spray-bg {
|
.spray-bg {
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle, rgba(255, 26, 140, 0.09) 1px, transparent 1px),
|
radial-gradient(circle, rgba(255, 26, 140, 0.09) 1px, transparent 1px),
|
||||||
radial-gradient(circle, rgba(0, 200, 255, 0.06) 1px, transparent 1px);
|
radial-gradient(circle, rgba(0, 200, 255, 0.06) 1px, transparent 1px);
|
||||||
background-size: 18px 18px, 29px 29px;
|
background-size:
|
||||||
background-position: 0 0, 9px 9px;
|
18px 18px,
|
||||||
|
29px 29px;
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
9px 9px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Noise overlay ──────────────────────────────────────────── */
|
/* ── Noise overlay ──────────────────────────────────────────── */
|
||||||
@@ -399,10 +487,15 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
animation: marquee-scroll 32s linear infinite;
|
animation: marquee-scroll 32s linear infinite;
|
||||||
}
|
}
|
||||||
.marquee-track:hover { animation-play-state: paused; }
|
.marquee-track:hover {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Section rules ──────────────────────────────────────────── */
|
/* ── Section rules ──────────────────────────────────────────── */
|
||||||
.section-rule { border: none; border-top: 1px solid var(--color-zinc); }
|
.section-rule {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-zinc);
|
||||||
|
}
|
||||||
|
|
||||||
.section-rule-electric {
|
.section-rule-electric {
|
||||||
border: none;
|
border: none;
|
||||||
@@ -419,9 +512,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── Neon glow utilities ────────────────────────────────────── */
|
/* ── Neon glow utilities ────────────────────────────────────── */
|
||||||
.neon-heat { box-shadow: 0 0 24px rgba(255, 26, 140, 0.35), 0 0 60px rgba(255, 26, 140, 0.08); }
|
.neon-heat {
|
||||||
.neon-pulse { box-shadow: 0 0 24px rgba(155, 0, 255, 0.35), 0 0 60px rgba(155, 0, 255, 0.08); }
|
box-shadow:
|
||||||
.neon-frost { box-shadow: 0 0 24px rgba( 0, 200, 255, 0.35), 0 0 60px rgba( 0, 200, 255, 0.08); }
|
0 0 24px rgba(255, 26, 140, 0.35),
|
||||||
|
0 0 60px rgba(255, 26, 140, 0.08);
|
||||||
|
}
|
||||||
|
.neon-pulse {
|
||||||
|
box-shadow:
|
||||||
|
0 0 24px rgba(155, 0, 255, 0.35),
|
||||||
|
0 0 60px rgba(155, 0, 255, 0.08);
|
||||||
|
}
|
||||||
|
.neon-frost {
|
||||||
|
box-shadow:
|
||||||
|
0 0 24px rgba(0, 200, 255, 0.35),
|
||||||
|
0 0 60px rgba(0, 200, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Prose editorial ────────────────────────────────────────── */
|
/* ── Prose editorial ────────────────────────────────────────── */
|
||||||
.prose-editorial {
|
.prose-editorial {
|
||||||
@@ -432,15 +537,25 @@
|
|||||||
max-width: 72ch;
|
max-width: 72ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose-editorial > * + * { margin-top: 1.6em; }
|
.prose-editorial > * + * {
|
||||||
|
margin-top: 1.6em;
|
||||||
|
}
|
||||||
|
|
||||||
.prose-editorial strong { color: var(--color-paper); font-weight: 600; }
|
.prose-editorial strong {
|
||||||
.prose-editorial em { font-style: italic; color: var(--color-paper); }
|
color: var(--color-paper);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.prose-editorial em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--color-paper);
|
||||||
|
}
|
||||||
|
|
||||||
.prose-editorial a {
|
.prose-editorial a {
|
||||||
color: var(--color-heat);
|
color: var(--color-heat);
|
||||||
border-bottom: 1px solid rgba(255, 26, 140, 0.35);
|
border-bottom: 1px solid rgba(255, 26, 140, 0.35);
|
||||||
transition: border-color 0.2s, color 0.2s;
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
color 0.2s;
|
||||||
}
|
}
|
||||||
.prose-editorial a:hover {
|
.prose-editorial a:hover {
|
||||||
color: var(--color-frost);
|
color: var(--color-frost);
|
||||||
@@ -457,7 +572,9 @@
|
|||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
.prose-editorial blockquote p { margin: 0; }
|
.prose-editorial blockquote p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.prose-editorial h2 {
|
.prose-editorial h2 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
@@ -509,7 +626,9 @@
|
|||||||
background: linear-gradient(90deg, var(--color-heat), var(--color-pulse), var(--color-frost));
|
background: linear-gradient(90deg, var(--color-heat), var(--color-pulse), var(--color-frost));
|
||||||
box-shadow: 0 0 12px rgba(255, 26, 140, 0.6);
|
box-shadow: 0 0 12px rgba(255, 26, 140, 0.6);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
transition: width 0.4s var(--ease-out-expo), opacity 0.3s ease;
|
transition:
|
||||||
|
width 0.4s var(--ease-out-expo),
|
||||||
|
opacity 0.3s ease;
|
||||||
width: 0;
|
width: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
@@ -517,62 +636,128 @@
|
|||||||
|
|
||||||
/* ── Utilities ────────────────────────────────────────────────── */
|
/* ── Utilities ────────────────────────────────────────────────── */
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|
||||||
.gutter-x {
|
.gutter-x {
|
||||||
padding-left: clamp(1rem, 4vw, 3rem);
|
padding-left: clamp(1rem, 4vw, 3rem);
|
||||||
padding-right: clamp(1rem, 4vw, 3rem);
|
padding-right: clamp(1rem, 4vw, 3rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Aspect ratios */
|
/* Aspect ratios */
|
||||||
.aspect-editorial { aspect-ratio: 3 / 4; }
|
.aspect-editorial {
|
||||||
.aspect-cinema { aspect-ratio: 21 / 9; }
|
aspect-ratio: 3 / 4;
|
||||||
.aspect-portrait { aspect-ratio: 2 / 3; }
|
}
|
||||||
|
.aspect-cinema {
|
||||||
|
aspect-ratio: 21 / 9;
|
||||||
|
}
|
||||||
|
.aspect-portrait {
|
||||||
|
aspect-ratio: 2 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
/* Text wrapping */
|
/* Text wrapping */
|
||||||
.text-balance { text-wrap: balance; }
|
.text-balance {
|
||||||
.text-pretty { text-wrap: pretty; }
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
.text-pretty {
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
/* Offset shadow */
|
/* Offset shadow */
|
||||||
.shadow-comic { box-shadow: 5px 5px 0 var(--color-heat); }
|
.shadow-comic {
|
||||||
.shadow-comic-hot { box-shadow: 5px 5px 0 var(--color-heat); }
|
box-shadow: 5px 5px 0 var(--color-heat);
|
||||||
.shadow-comic-volt { box-shadow: 5px 5px 0 var(--color-lime); }
|
}
|
||||||
.shadow-comic-ice { box-shadow: 5px 5px 0 var(--color-frost); }
|
.shadow-comic-hot {
|
||||||
|
box-shadow: 5px 5px 0 var(--color-heat);
|
||||||
|
}
|
||||||
|
.shadow-comic-volt {
|
||||||
|
box-shadow: 5px 5px 0 var(--color-lime);
|
||||||
|
}
|
||||||
|
.shadow-comic-ice {
|
||||||
|
box-shadow: 5px 5px 0 var(--color-frost);
|
||||||
|
}
|
||||||
|
|
||||||
/* Text stroke — hollow headline */
|
/* Text stroke — hollow headline */
|
||||||
.text-stroke-gold { -webkit-text-stroke: 1.5px var(--color-heat); color: transparent; }
|
.text-stroke-gold {
|
||||||
.text-stroke-paper { -webkit-text-stroke: 1.5px var(--color-paper); color: transparent; }
|
-webkit-text-stroke: 1.5px var(--color-heat);
|
||||||
.text-stroke-heat { -webkit-text-stroke: 1.5px var(--color-heat); color: transparent; }
|
color: transparent;
|
||||||
.text-stroke-frost { -webkit-text-stroke: 1.5px var(--color-frost); color: transparent; }
|
}
|
||||||
|
.text-stroke-paper {
|
||||||
|
-webkit-text-stroke: 1.5px var(--color-paper);
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.text-stroke-heat {
|
||||||
|
-webkit-text-stroke: 1.5px var(--color-heat);
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.text-stroke-frost {
|
||||||
|
-webkit-text-stroke: 1.5px var(--color-frost);
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Scrollbar hide */
|
/* Scrollbar hide */
|
||||||
.scrollbar-hide { scrollbar-width: none; -ms-overflow-style: none; }
|
.scrollbar-hide {
|
||||||
.scrollbar-hide::-webkit-scrollbar { display: none; }
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Keyframes ────────────────────────────────────────────────── */
|
/* ── Keyframes ────────────────────────────────────────────────── */
|
||||||
@keyframes gradient-shift {
|
@keyframes gradient-shift {
|
||||||
0% { background-position: 0% 50%; }
|
0% {
|
||||||
50% { background-position: 100% 50%; }
|
background-position: 0% 50%;
|
||||||
100% { background-position: 0% 50%; }
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes marquee-scroll {
|
@keyframes marquee-scroll {
|
||||||
to { transform: translateX(-50%); }
|
to {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scroll-line {
|
@keyframes scroll-line {
|
||||||
0% { transform: translateY(-100%); opacity: 0; }
|
0% {
|
||||||
20% { opacity: 1; }
|
transform: translateY(-100%);
|
||||||
80% { opacity: 1; }
|
opacity: 0;
|
||||||
100% { transform: translateY(100%); opacity: 0; }
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Logo glitch animation ───────────────────────────────────── */
|
/* ── Logo glitch animation ───────────────────────────────────── */
|
||||||
@keyframes glitch-clip {
|
@keyframes glitch-clip {
|
||||||
0%, 88%, 100% { transform: translate(0); clip-path: none; }
|
0%,
|
||||||
90% { transform: translate(-3px, 1px); clip-path: polygon(0 15%, 100% 15%, 100% 40%, 0 40%); }
|
88%,
|
||||||
92% { transform: translate(3px, -1px); clip-path: polygon(0 60%, 100% 60%, 100% 78%, 0 78%); }
|
100% {
|
||||||
94% { transform: translate(0); clip-path: none; }
|
transform: translate(0);
|
||||||
|
clip-path: none;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
transform: translate(-3px, 1px);
|
||||||
|
clip-path: polygon(0 15%, 100% 15%, 100% 40%, 0 40%);
|
||||||
|
}
|
||||||
|
92% {
|
||||||
|
transform: translate(3px, -1px);
|
||||||
|
clip-path: polygon(0 60%, 100% 60%, 100% 78%, 0 78%);
|
||||||
|
}
|
||||||
|
94% {
|
||||||
|
transform: translate(0);
|
||||||
|
clip-path: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover .logo-glitch {
|
a:hover .logo-glitch {
|
||||||
@@ -591,15 +776,24 @@ footer .logo-img {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes logo-float {
|
@keyframes logo-float {
|
||||||
0%, 100% { transform: translateY(0); filter: drop-shadow(0 0 8px rgba(255, 26, 140, 0.35)); }
|
0%,
|
||||||
50% { transform: translateY(-4px); filter: drop-shadow(0 6px 14px rgba(255, 26, 140, 0.6)); }
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
filter: drop-shadow(0 0 8px rgba(255, 26, 140, 0.35));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
filter: drop-shadow(0 6px 14px rgba(255, 26, 140, 0.6));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Nav & footer link hover polish ─────────────────────────── */
|
/* ── Nav & footer link hover polish ─────────────────────────── */
|
||||||
|
|
||||||
/* Desktop nav links — lift + glow on hover */
|
/* Desktop nav links — lift + glow on hover */
|
||||||
header nav a.label {
|
header nav a.label {
|
||||||
transition: color 0.2s var(--ease-sharp), transform 0.2s var(--ease-out-expo);
|
transition:
|
||||||
|
color 0.2s var(--ease-sharp),
|
||||||
|
transform 0.2s var(--ease-out-expo);
|
||||||
}
|
}
|
||||||
header nav a.label:hover {
|
header nav a.label:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@@ -612,7 +806,9 @@ header nav a.label span {
|
|||||||
|
|
||||||
/* Footer links — subtle lift */
|
/* Footer links — subtle lift */
|
||||||
footer nav a.label {
|
footer nav a.label {
|
||||||
transition: color 0.2s var(--ease-sharp), transform 0.2s var(--ease-out-expo);
|
transition:
|
||||||
|
color 0.2s var(--ease-sharp),
|
||||||
|
transform 0.2s var(--ease-out-expo);
|
||||||
}
|
}
|
||||||
footer nav a.label:hover {
|
footer nav a.label:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
@@ -636,10 +832,15 @@ footer nav a.label:hover svg {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes page-out {
|
@keyframes page-out {
|
||||||
to { opacity: 0; transform: translateY(-10px); }
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes page-in {
|
@keyframes page-in {
|
||||||
from { opacity: 0; transform: translateY(14px); }
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
bar.style.width = "100%";
|
bar.style.width = "100%";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
bar.style.opacity = "0";
|
bar.style.opacity = "0";
|
||||||
setTimeout(() => { bar.style.width = "0"; }, 350);
|
setTimeout(() => {
|
||||||
|
bar.style.width = "0";
|
||||||
|
}, 350);
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
@@ -37,9 +39,11 @@ function initLazyVideos(root) {
|
|||||||
observer.unobserve(video);
|
observer.unobserve(video);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ rootMargin: "300px" }
|
{ rootMargin: "300px" },
|
||||||
);
|
);
|
||||||
(root || document).querySelectorAll("video[data-src]").forEach((v) => observer.observe(v));
|
(root || document)
|
||||||
|
.querySelectorAll("video[data-src]")
|
||||||
|
.forEach((v) => observer.observe(v));
|
||||||
}
|
}
|
||||||
|
|
||||||
initLazyVideos();
|
initLazyVideos();
|
||||||
@@ -67,7 +71,9 @@ document.body.addEventListener("htmx:historyRestore", () => {
|
|||||||
bar.style.width = "100%";
|
bar.style.width = "100%";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
bar.style.opacity = "0";
|
bar.style.opacity = "0";
|
||||||
setTimeout(() => { bar.style.width = "0"; }, 350);
|
setTimeout(() => {
|
||||||
|
bar.style.width = "0";
|
||||||
|
}, 350);
|
||||||
}, 150);
|
}, 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,4 +87,3 @@ document.body.addEventListener("htmx:historyRestore", () => {
|
|||||||
window.scrollTo({ top: 0, behavior: "instant" });
|
window.scrollTo({ top: 0, behavior: "instant" });
|
||||||
window.dispatchEvent(new Event("scroll"));
|
window.dispatchEvent(new Event("scroll"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ title: "About Pivoine"
|
|||||||
description: "A magazine dedicated to the intersection of artificial intelligence and aesthetic culture."
|
description: "A magazine dedicated to the intersection of artificial intelligence and aesthetic culture."
|
||||||
---
|
---
|
||||||
|
|
||||||
*Pivoine* is an independent editorial project exploring how machine intelligence reshapes visual culture.
|
_Pivoine_ is an independent editorial project exploring how machine intelligence reshapes visual culture.
|
||||||
|
|
||||||
We publish editorial features, artist profiles, and critical essays about AI-generated art — approaching this emerging medium with the same rigour and aesthetic seriousness as any other.
|
We publish editorial features, artist profiles, and critical essays about AI-generated art — approaching this emerging medium with the same rigour and aesthetic seriousness as any other.
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ The name comes from the French word for **peony** — a flower that is both extr
|
|||||||
|
|
||||||
## What We Publish
|
## What We Publish
|
||||||
|
|
||||||
We are interested in the **editorial dimension** of AI image-making. Not the tools, not the prompts, not the speed records — but the *work* itself and the questions it raises about authorship, aesthetics, and what it means to make an image in an age when images are free.
|
We are interested in the **editorial dimension** of AI image-making. Not the tools, not the prompts, not the speed records — but the _work_ itself and the questions it raises about authorship, aesthetics, and what it means to make an image in an age when images are free.
|
||||||
|
|
||||||
Each feature is a collaboration between a human sensibility and a machine process. The artists and authors we publish treat the generator as a medium, not a shortcut.
|
Each feature is a collaboration between a human sensibility and a machine process. The artists and authors we publish treat the generator as a medium, not a shortcut.
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: "Valknar"
|
title: "Valknar"
|
||||||
name: "Valknar"
|
name: "Valknar"
|
||||||
bio: "Valknar treats diffusion models as a engine of contradiction — pitting the cute against the grotesque, the opulent against the decayed, and the familiar against the deeply wrong. The results are cinematic concept images that feel like stills from films that should not exist."
|
bio: "Valknar builds cinematic AI concept art at the intersection of gothic darkness, mythological grandeur, and neon-soaked noir — images that feel like stills from films that were never made, but should have been."
|
||||||
social:
|
social:
|
||||||
instagram: "valknarix"
|
instagram: "valknarix"
|
||||||
x: "valknar100"
|
x: "valknar100"
|
||||||
---
|
---
|
||||||
|
|
||||||
Valknar approaches AI generation as a collision course. The work takes familiar cultural touchstones — video game icons, children's characters, Renaissance still life — and forces them through genre frameworks they were never designed to survive: survival horror, Gigeresque biomechanics, neo-Tokyo neon, Burtonesque gothic.
|
Valknar works at the intersection of the dark and the epic. The images move between worlds — candlelit vampire chambers and rain-slicked Neo-Tokyo streets, Norse battlefields at the hour the last warrior falls, ancient Japanese throne rooms suspended in a single moment of ceremony. Across all of it, the aesthetic constants are atmosphere and weight.
|
||||||
|
|
||||||
The technique is less about coaxing beauty from the model and more about engineering productive discomfort. Vivid colour is deployed against desolation. Opulence is buried in decay. The cinematic framing is deliberate — wide shots, hero angles, macro details — borrowed from the language of film concept art and applied to subjects the camera would not normally linger on.
|
Each scene is engineered with the specificity of a cinematographer's brief: lighting quality, lens choice, colour science, and the physical texture of materials described precisely enough that the model has no choice but to commit. The goal is never decoration — it is to make a still image feel like a held breath, a moment poised just before or just after something irreversible.
|
||||||
|
|
||||||
Based in Stuttgart.
|
Based in Stuttgart.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ description: "Legal information and editorial responsibility."
|
|||||||
|
|
||||||
## Editorial Policy
|
## Editorial Policy
|
||||||
|
|
||||||
*Pivoine* is an independent publication. All AI-generated images and videos published on this site are the creative work of the named authors using various generative AI tools. The editorial team curates, commissions, and publishes this work under the publication's name.
|
_Pivoine_ is an independent publication. All AI-generated images and videos published on this site are the creative work of the named authors using various generative AI tools. The editorial team curates, commissions, and publishes this work under the publication's name.
|
||||||
|
|
||||||
## Copyright
|
## Copyright
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 7.0 MiB |
|
Before Width: | Height: | Size: 5.1 MiB |
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
title: "The Pale Pour"
|
title: "The Pale Pour"
|
||||||
description: "A skeletal Grim Reaper tends bar in a subterranean Gothic absinthe den, where the Green Fairy's bioluminescent fog coils between candlelit bottles and tarnished mirrors."
|
description: "A skeletal Grim Reaper tends bar in a subterranean Gothic absinthe den, where the Green Fairy's bioluminescent fog coils between candlelit bottles and tarnished mirrors."
|
||||||
date: 2026-05-07
|
date: 2026-05-07T18:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: true
|
featured: true
|
||||||
categories:
|
categories:
|
||||||
- Dark
|
- Dark
|
||||||
- Surrealist
|
- Surrealist
|
||||||
@@ -17,8 +17,8 @@ tags:
|
|||||||
- Macabre
|
- Macabre
|
||||||
slug: the-pale-pour
|
slug: the-pale-pour
|
||||||
prompt:
|
prompt:
|
||||||
scene: "A cinematic, wide-angle interior shot of a subterranean, rustic Gothic Absinthe bar. The atmosphere is thick with a swirling, ethereal green bioluminescent fog (\"The Green Fairy\"). In the center, a skeletal Grim Reaper stands behind a heavy, scarred oak bar, draped in tattered, ink-black hooded robes; his bony hands are delicately preparing a traditional absinthe drip over a silver slotted spoon and sugar cube."
|
scene: 'A cinematic, wide-angle interior shot of a subterranean, rustic Gothic Absinthe bar. The atmosphere is thick with a swirling, ethereal green bioluminescent fog ("The Green Fairy"). In the center, a skeletal Grim Reaper stands behind a heavy, scarred oak bar, draped in tattered, ink-black hooded robes; his bony hands are delicately preparing a traditional absinthe drip over a silver slotted spoon and sugar cube.'
|
||||||
environment: "Vaulted stone ceilings with cracked ribs, dripping wax from oversized iron chandeliers, walls lined with dusty, unlabeled apothecary bottles and antique mirrors showing tarnished reflections. Plush, rotting emerald velvet barstools and flickering candlelight."
|
environment: "Vaulted stone ceilings with cracked ribs, dripping wax from oversized iron chandeliers, walls lined with dusty, unlabeled apothecary bottles and antique mirrors showing tarnished reflections. Plush, rotting emerald velvet barstools and flickering candlelight."
|
||||||
style: "Moody chiaroscuro lighting, deep shadows contrasted with vibrant neon-emerald highlights. Art Nouveau accents, hyper-realistic textures of bone, aged wood, and etched glass. Dark Victorian aesthetic, macabre elegance, 8k resolution, photorealistic with a touch of oil painting gloom."
|
style: "Moody chiaroscuro lighting, deep shadows contrasted with vibrant neon-emerald highlights. Art Nouveau accents, hyper-realistic textures of bone, aged wood, and etched glass. Dark Victorian aesthetic, macabre elegance, 8k resolution, photorealistic with a touch of oil painting gloom."
|
||||||
video: "Cinematic dolly shot: The grim reaper in a dark cloak stands at a dimly lit Absinth bar, surrounded by glowing green potions. The camera slowly zooms in on the figure as he dunks a spoon in the green potion with one hand and with his other hand's index finger igniting the spoon into a small flame."
|
video: "The Reaper's skeletal right hand brings a long match — a single small orange flame — down to the absinthe-soaked sugar cube resting on the silver slotted spoon. A clean blue-gold flame catches immediately and burns steadily on the sugar. Caramelised sugar begins to drip slowly through the slots into the glass below, clouding the absinthe pale green. The green bioluminescent fog swirls gently as the firelight touches it. The Reaper is otherwise completely motionless. Camera holds static throughout."
|
||||||
---
|
---
|
||||||
|
|||||||
|
After Width: | Height: | Size: 6.9 MiB |
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "The Count's Chamber"
|
||||||
|
description: "The room speaks before its master does — an ancient Gothic chamber of firelight and shadow, an armchair that still holds the warmth of centuries, and a wine glass of blood on a small table, patiently waiting."
|
||||||
|
date: 2026-05-08T12:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Gothic
|
||||||
|
tags:
|
||||||
|
- Dracula
|
||||||
|
- Vampire
|
||||||
|
- Gothic
|
||||||
|
- Interior
|
||||||
|
- Atmospheric
|
||||||
|
- Horror
|
||||||
|
- Candlelight
|
||||||
|
- Victorian
|
||||||
|
slug: the-counts-chamber
|
||||||
|
prompt:
|
||||||
|
scene: "A grand, wide-angle establishing shot of Count Dracula's opulent private chamber — vast and magnificent, the full room revealed in breathtaking detail. The camera is pulled back to show the entire space: soaring vaulted ceilings of dressed stone, twenty feet high, hung with an enormous tattered chandelier of black iron and a hundred burnt-down black candles still smouldering. Dead centre of the room, angled toward a monumental double fireplace of carved obsidian and grey marble — large enough to stand inside — sits a throne-like armchair of deep burgundy leather and black wrought-iron rivets, its cushion subtly depressed. To its right, a small ornate side table holds a single crystal wine glass of deep crimson blood, an uncorked decanter, a withered black rose, and a pocket watch stopped at midnight. The room extends dramatically on all sides around this centrepiece."
|
||||||
|
environment: "The chamber is a palace of accumulated dark splendour: the walls are panelled in age-blackened oak to half-height, above which bare stone is hung with an overwhelming collection of enormous oil portraits in gilded frames — dozens of them, floor to ceiling, every painted eye subtly converging on the armchair. A library wall of floor-to-ceiling shelves groans under thousands of leather-bound tomes, accessible by a rolling iron ladder on a brass rail. Opposite, a series of three immense arched windows, each twelve feet tall, shuttered in iron — thin blades of cold blue moonlight falling in parallel stripes across the vast Persian rug below, a masterwork of burgundy, black and gold that covers the entire flagstone floor. A grand pipe organ of tarnished black metal occupies one corner, sheet music still on its stand. Suits of armour flank the fireplace. A writing desk of dark mahogany near the window is buried in maps, correspondence sealed with red wax, and quill pens in a silver stand. The grandfather clock in the far corner reads midnight. Every surface is a testament to centuries of wealth and absolute solitude."
|
||||||
|
style: "Pure Gothic atmospheric horror — the tension of an empty room that is not truly empty. Vermeer-level interior lighting mastery: firelight as the dominant warm source casting long amber shadows, cold moonlight blades from the shuttered window as counterpoint, candlelight filling the middle distance with wavering amber halos. Deep focus throughout — every surface rich with aged texture: cracked leather, tarnished silver, dusty velvet, wet stone. The wine glass in sharp focus, blood surface catching the light with impossible depth. Colour palette: deep burgundy, ebony, aged ivory, old gold, cold blue. 8K resolution, photorealistic oil painting render in the tradition of Dutch Golden Age interior painting with Gothic horror overtones."
|
||||||
|
video: "The camera opens on the fireplace — flames and candles burning in total silence, no sound but the occasional soft pop of embers. A slow, deliberate dolly back reveals the armchair, then the side table, then the wine glass. Atmosphere: the dread of a room that is waiting for its owner to return."
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 7.2 MiB |
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "The Last Valkyrie"
|
||||||
|
description: "She walks the field when the battle is already over — magnificent, heavily armed, towering above the fallen — choosing among the dead with the cold authority of a goddess who has never known defeat."
|
||||||
|
date: 2026-05-08T14:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Fantasy
|
||||||
|
tags:
|
||||||
|
- Valkyrie
|
||||||
|
- Viking
|
||||||
|
- Battlefield
|
||||||
|
- Norse Mythology
|
||||||
|
- Epic
|
||||||
|
- Warrior
|
||||||
|
- Mythology
|
||||||
|
- Cinematic
|
||||||
|
slug: the-last-valkyrie
|
||||||
|
prompt:
|
||||||
|
scene: "A magnificent, heavily armed and armoured Valkyrie stands alone on a vast epic battlefield littered with fallen Vikings, rendered in breathtaking cinematic grandeur. She is colossal in presence — full-figure, centre frame — clad in immaculate yet battle-tested armour of layered steel and burnished gold, intricately engraved with Norse knotwork and raven motifs. A great winged helm crowns her head, her long silver-white hair whipping in the wind. In one gauntleted hand she holds a massive war spear crackling with pale lightning; in the other, a round shield of black iron and gold bearing the mark of Odin. Her expression is absolute, divine authority — neither cruel nor merciful, simply inevitable. Around her in every direction, hundreds of fallen Viking warriors lie across the mud and stone, their shields shattered, their banners torn."
|
||||||
|
environment: "A vast Norse battlefield at the moment after the last blow has fallen — dusk sky of deep violet and bruised amber, storm clouds massing overhead shot through with distant lightning. The field itself is a panorama of epic carnage rendered with mythic gravity: broken longships beached in the distance, ravens circling in vast spirals overhead, shield walls collapsed into tangles of iron and oak. Mist rolls across the ground in slow waves. In the far background, the silhouette of a great hall burns against the sky. A single riderless horse stands motionless among the fallen. The Valkyrie's armour catches the last dying light of the sun as the storm closes in."
|
||||||
|
style: "Norse epic fantasy at maximum cinematic scale — the visual grandeur of God of War Ragnarök meets the painterly sweep of classic Frank Frazetta barbarian art and the mythic weight of Zack Snyder's 300. Hyper-detailed armour engraving, fur cloaks, weathered iron and aged leather across the fallen warriors. Dramatic upward low-angle composition placing the Valkyrie against the storming sky. Volumetric god-rays breaking through storm clouds onto the battlefield. Rich desaturated colour grade: steel grey, tarnished gold, deep violet sky, muted blood-red earth. Wind-motion blur on hair, cloaks, and torn banners. 8K resolution, photorealistic epic fantasy render with oil-painting atmosphere."
|
||||||
|
video: "The Valkyrie stands motionless. Storm wind slowly stirs her hair and cloak. The Valkyrie stands motionless. Storm wind slowly stirs her hair and cloak. Lightning pulses once in her spear. A single raven descends and lands on her outstretched shield arm. She turns her head slowly to survey the field. Camera holds static throughout — no movement, no cut."
|
||||||
|
---
|
||||||
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 6.4 MiB |
|
Before Width: | Height: | Size: 5.7 MiB |
@@ -1,12 +1,12 @@
|
|||||||
---
|
---
|
||||||
title: "The Warlord's Hour"
|
title: "The Warlord's Hour"
|
||||||
description: "A fearsome samurai warlord, clad in magnificent lacquered armour, receives the quiet ceremony of sake from a geisha — power and grace suspended in a single, breathless moment on an ancient throne."
|
description: "A fearsome samurai warlord, clad in magnificent lacquered armour, receives the quiet ceremony of sake from a geisha — power and grace suspended in a single, breathless moment on an ancient throne."
|
||||||
date: 2026-05-08
|
date: 2026-05-08T10:00:00+02:00
|
||||||
author: "valknar"
|
author: "valknar"
|
||||||
featured: false
|
featured: false
|
||||||
categories:
|
categories:
|
||||||
- Historical
|
|
||||||
- Dark
|
- Dark
|
||||||
|
- Historical
|
||||||
tags:
|
tags:
|
||||||
- Samurai
|
- Samurai
|
||||||
- Japan
|
- Japan
|
||||||
@@ -21,5 +21,5 @@ prompt:
|
|||||||
scene: "A commanding samurai warlord sits in absolute stillness upon an ancient lacquered throne of dark cedar and iron, set within a vast feudal Japanese great hall. His armour is extraordinary — full ō-yoroi in deep crimson and black lacquered plates, gilded mon crests on the shoulder guards, a fearsome menpō face mask pushed up to reveal a scarred, weathered face of absolute authority. His katana rests across his knees, unsheathed. Before him, a geisha in a pristine white and gold furisode kimono kneels gracefully on a tatami mat, both hands extended, presenting a lacquered sake tray with a single ceramic cup. Her gaze is downcast, her white-painted face a mask of serene composure."
|
scene: "A commanding samurai warlord sits in absolute stillness upon an ancient lacquered throne of dark cedar and iron, set within a vast feudal Japanese great hall. His armour is extraordinary — full ō-yoroi in deep crimson and black lacquered plates, gilded mon crests on the shoulder guards, a fearsome menpō face mask pushed up to reveal a scarred, weathered face of absolute authority. His katana rests across his knees, unsheathed. Before him, a geisha in a pristine white and gold furisode kimono kneels gracefully on a tatami mat, both hands extended, presenting a lacquered sake tray with a single ceramic cup. Her gaze is downcast, her white-painted face a mask of serene composure."
|
||||||
environment: "The great hall is cavernous and dimly lit — massive wooden pillars rising to a shadowed ceiling hung with battle banners bearing clan crests in faded red and black. Rice paper shoji screens along the walls glow faintly with the light of paper lanterns beyond. Incense smoke drifts in slow diagonals across the space. Scattered armour stands and weapon racks line the periphery. Sparse cherry blossom petals have blown in through an unseen open screen, settling on the tatami floor. Outside, the muffled sound of a distant war drum."
|
environment: "The great hall is cavernous and dimly lit — massive wooden pillars rising to a shadowed ceiling hung with battle banners bearing clan crests in faded red and black. Rice paper shoji screens along the walls glow faintly with the light of paper lanterns beyond. Incense smoke drifts in slow diagonals across the space. Scattered armour stands and weapon racks line the periphery. Sparse cherry blossom petals have blown in through an unseen open screen, settling on the tatami floor. Outside, the muffled sound of a distant war drum."
|
||||||
style: "Cinematic feudal Japan, hyper-realistic textures of lacquered armour, raw silk, and aged cedar. Dramatic chiaroscuro lighting — a single strong source from a paper lantern above illuminates the warlord's armour and the geisha's kimono, deep shadows consuming the hall behind. Rich colour palette: crimson, black, ivory, gold. Inspired by Akira Kurosawa's compositional gravity and the painterly depth of ukiyo-e woodblock prints. Ultra-detailed fabric and metalwork, 8K resolution, photorealistic with cinematic grain."
|
style: "Cinematic feudal Japan, hyper-realistic textures of lacquered armour, raw silk, and aged cedar. Dramatic chiaroscuro lighting — a single strong source from a paper lantern above illuminates the warlord's armour and the geisha's kimono, deep shadows consuming the hall behind. Rich colour palette: crimson, black, ivory, gold. Inspired by Akira Kurosawa's compositional gravity and the painterly depth of ukiyo-e woodblock prints. Ultra-detailed fabric and metalwork, 8K resolution, photorealistic with cinematic grain."
|
||||||
video: "Camera opens on a slow push through the darkened great hall — battle banners drifting almost imperceptibly in unseen air. The warlord's silhouette emerges from shadow first, the glint of his unsheathed blade catching light. The camera settles into a wide static shot as the geisha glides into frame from the left, each step a whisper. She kneels. Her arms extend with the tray. A long, suspended silence. The warlord's gauntleted hand moves — slowly, deliberately — and lifts the cup. Incense smoke drifts between them. A single cherry blossom petal falls onto the tatami. Atmosphere: absolute power held in ceremonial stillness, the calm before or after great violence."
|
video: "Camera opens on a slow push through the darkened great hall — battle banners drifting almost imperceptibly in unseen air. The warlord's silhouette emerges from shadow first, the glint of his unsheathed blade catching light. The geisha kneels. Her arms extend with the tray. A long, suspended silence. The warlord's gauntleted hand moves — slowly, deliberately — and lifts the cup. Incense smoke drifts between them. A single cherry blossom petal falls onto the tatami. Atmosphere: absolute power held in ceremonial stillness, the calm before or after great violence."
|
||||||
---
|
---
|
||||||
|
|||||||
|
After Width: | Height: | Size: 7.3 MiB |
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "Iron Rain"
|
||||||
|
description: "A colossal combat mech holds the intersection — armour scarred by a hundred engagements, missile pods tracking, plasma blade coiled and ready — while Neo-Tokyo dissolves in rain around it and no one dares move."
|
||||||
|
date: 2026-05-10T18:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: true
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Sci-Fi
|
||||||
|
tags:
|
||||||
|
- Mech
|
||||||
|
- Cyberpunk
|
||||||
|
- Neo-Tokyo
|
||||||
|
- Rain
|
||||||
|
- Night
|
||||||
|
- Futuristic
|
||||||
|
- War Machine
|
||||||
|
- Combat
|
||||||
|
slug: iron-rain
|
||||||
|
prompt:
|
||||||
|
scene: "A colossal combat mech stands motionless at a rain-soaked intersection in futuristic Neo-Tokyo, late at night. Full-figure, wide shot — the entire machine visible from ground to crown. The mech is massively armoured: layered titanium-alloy plating battle-scarred and oil-stained, hydraulic joints venting live steam into the cold night air, twin missile pods on both shoulders, a rotary gatling cannon where the left arm should be, a plasma blade housing on the right forearm crackling with suppressed blue-white energy. The torso bears a faded corporate military insignia, half-obscured by scoring and carbon residue. The head unit is a featureless armoured dome with a single wide horizontal optical band pulsing amber — slowly sweeping, scanning. Rain hammers every surface: bouncing off armour plates in silver droplets, running in sheets down the cockpit glass, pooling in neon-lit puddles around the mech's enormous hydraulic feet. It is completely still. Everything about it communicates that this stillness is temporary."
|
||||||
|
environment: "A deep rain-canyon of Neo-Tokyo at maximum atmospheric density. Corporate megablock towers rise on both sides, walls plastered floor-to-roof in overlapping holographic advertisements and kanji signage — pink, amber, cyan, electric green — their light bleeding and fracturing across every rain-slicked surface. Power cables and elevated transit rails crisscross the gap overhead. The street is empty: abandoned street vendor carts, an overturned delivery vehicle still sparking, shattered glass across the tarmac. A police cordon barrier lies flattened in the background. In the far distance, a mega-structure rises into low cloud cover, upper floors lost in rain-haze and sweeping searchlight beams. Everything civilian has been evacuated. The city holds its breath."
|
||||||
|
style: "Hyper-realistic cyberpunk war photography. Blade Runner 2049 colour science and atmospheric density meets Pacific Rim's mechanical scale — rendered through the lens of a war correspondent on the ground. Photorealistic wet-surface caustics: neon colour bleeding across every puddle with physical accuracy, rain streaks rendered with motion-blur density, steam particles volumetrically lit. Three-point cinematic lighting: dominant amber sodium vapour from below, cold blue-white from a searchlight above, magenta-pink neon spill from signage at left. The mech rendered with absolute material fidelity — scratched titanium highlights, oil-dark joint cavities, heat-scorched venting panels. 8K photorealistic render, anamorphic lens with characteristic oval bokeh, cinematic colour grade: deep teal shadows, amber-orange midtones, magenta-pink neon highlights."
|
||||||
|
video: "Rain falls continuously. The mech raises the hand, which doesn't hold the gun, to show a traffic stop gesture. A single vehicle creeps into frame from the right. The mech holds position. Neon reflections ripple in the puddles. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 6.8 MiB |
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "The Drowned Queen"
|
||||||
|
description: "She sails still — centuries dead, cursed to never reach port — her black hull half-fog and half-ruin, her skeletal crew working the rigging in silence, her tattered sails full of a wind that does not exist."
|
||||||
|
date: 2026-05-11T10:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Horror
|
||||||
|
tags:
|
||||||
|
- Ghost Ship
|
||||||
|
- Sea
|
||||||
|
- Storm
|
||||||
|
- Cursed
|
||||||
|
- Gothic
|
||||||
|
- Supernatural
|
||||||
|
- Maritime
|
||||||
|
- Dark Fantasy
|
||||||
|
- Horror
|
||||||
|
slug: the-drowned-queen
|
||||||
|
prompt:
|
||||||
|
scene: "A colossal, ancient three-masted galleon tears through a pitch-black storm sea — magnificent even in its ruin. Full broadside view, the entire vessel visible from shattered bowsprit to crumbling stern castle. The hull is black-tarred oak warped by centuries of saltwater, barnacle-encrusted below the waterline, riddled with cannonball scars and splintered planking that has never been repaired. The ship is half-transparent: in places the hull dissolves into spectral fog, revealing the skeletal crew working the rigging — ghostly forms in rotted sailor's garb, moving with supernatural, unhurried purpose. The sails are torn and tattered, yet fully billowed — not by wind but by some cursed breath — their canvas the grey of burial shrouds, faintly luminescent with cold green-blue light bleeding through every rent and hole. At the bowsprit, a figurehead of a crowned woman, hands outstretched as if pleading, mouth open in a silent scream. The ship's name — barely legible in corroded gold lettering on the stern: THE DROWNED QUEEN. Ghostly lanterns burn at intervals along the rails with cold blue flame. A massive anchor chain hangs loose from the bow, dragging into the black water below. The ship drives forward against physics and reason, utterly indifferent to the storm."
|
||||||
|
environment: "A catastrophic open-ocean storm at the absolute height of its fury — horizon and sky indistinguishable, swallowed together into a single churning black mass. Cathedral-scale wave mountains, twenty metres high, dark green-black and foam-streaked, crash against and through the ship's hull without slowing it. The sky is a unified ceiling of boiling storm cloud, split continuously by branching white lightning that illuminates everything in single brutal frames of pure white light. Rain falls in horizontal sheets so dense it becomes indistinguishable from mist and spray. In the far background, smaller human vessels are visible at the edges of the frame: listing, lights fading, overwhelmed. The ghost ship leaves no wake. Beneath the surface, a faint spectral glow emanates from below the hull, as if the abyss itself is lit from a cold source far below. Dead fish drift at the surface in the ship's path. The storm is not a backdrop — it is an arena, and the ship is its only master."
|
||||||
|
style: "Dark maritime horror at maximum epic scale — J.M.W. Turner's catastrophic storm paintings fused with the practical horror aesthetic of the Black Pearl and the supernatural grandeur of classic dark fantasy cinema. Photorealistic wet-surface rendering throughout: ocean spray, rain streaks, barnacle texture, rotted wood grain and corroded iron rendered with absolute material fidelity, ghost-translucency layered over physical decay. Wide cinematic composition — ship at three-quarter angle, occupying the full frame, ocean filling every edge to the corners. Three-point supernatural lighting: cold blue-green spectral glow emanating from the ship itself as its primary source, brutal overexposed white lightning from above, absolute void-black in all shadow cavities. Volumetric ocean spray and storm mist layered at multiple depths across the entire scene. Colour grade: near-monochromatic — desaturated deep ocean blues, tarnished blacks, cold grey-greens, spectral blue-white highlights on every ghostly element. 8K photorealistic render, wide anamorphic lens, maximum atmospheric particle density."
|
||||||
|
video: "Lightning strikes once — a single brutal white flash illuminates the entire scene for one frame. One enormous wave swell rises slowly against the hull, breaks into spray, and falls away. The tattered sails billow in a single gust of cursed wind. The figurehead's head turns, barely perceptibly, toward camera. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 6.6 MiB |
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "The Empty Throne"
|
||||||
|
description: "The throne room of the dead — obsidian and black iron, the river Styx visible through the far archway, Cerberus at rest beside the dais — is not empty because Hades is absent. It is empty because he is standing directly behind whoever is looking at it."
|
||||||
|
date: 2026-05-28T13:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Mythology
|
||||||
|
- Fantasy
|
||||||
|
tags:
|
||||||
|
- Hades
|
||||||
|
- Underworld
|
||||||
|
- Greek Mythology
|
||||||
|
- Throne
|
||||||
|
- Atmospheric
|
||||||
|
- Horror
|
||||||
|
- Cerberus
|
||||||
|
- Ancient
|
||||||
|
slug: the-empty-throne
|
||||||
|
prompt:
|
||||||
|
scene: "A grand, wide establishing shot of the throne room of Hades — the court of the Underworld rendered in its full architectural enormity — seen from the entrance archway looking in, as if the viewer has just arrived and has not yet been permitted to enter. The throne itself dominates: cut from a single piece of volcanic obsidian, its surfaces mirror-polished to perfection, its form severe and angular with no ornamentation and no concession to comfort — a chair designed to be occupied by something that does not tire, does not age, and does not require any of the things thrones are usually built to communicate. It sits elevated on a three-step dais of black marble, bare. To the right of the throne, a tall narrow window cut through the rock opens onto the only visible exterior: the river Styx, its surface moving with the faint luminescence of a million passing souls, its far bank lost in haze. On the far left of the room, curled in sleep on the bare stone floor beside the dais steps, Cerberus rests — three heads, each in a different direction, two sleeping, one not quite sleeping, its central pair of eyes half-open and reflecting the ambient light. The room is otherwise empty. Everything about its emptiness implies that this state is extremely recent."
|
||||||
|
environment: "The hall is monumental and carved from the living rock of the underworld itself — no dressed stone, no masonry, but the raw basalt of the deep earth shaped by will alone into columns, arches, and vaulted ceilings that disappear into dark heights far above. The columns are not straight: they lean slightly inward, as if the mountain is pressing down. From the ceiling, stalactites of black crystal hang at irregular intervals, each one emitting a faint cold phosphorescent glow — the only ambient light in the room. The floor is smooth as glass and slightly warm — the warmth of the deep earth, not fire. Along the walls, at irregular intervals, shallow niches hold soul-candles: flames that burn without wax, cold blue-white, each one representing a soul that has been here long enough to become part of the furniture. In the far corner, an hourglass the height of a man, its sand moving upward instead of down. From beyond the Styx window, very faint, the sound of water and the murmur of a crowd that never quite resolves into individual voices. Charon's boat is just barely visible on the river surface, moving toward a landing that is out of frame."
|
||||||
|
style: "Greek mythological architecture fused with the geological horror of a deep cave system — the civic grandeur of Minoan palace architecture and the formal severity of classical Greek design, both consumed and transformed by the living earth of the underworld into something pre-architectural, primordial. Lighting: the soul-candles as ambient cold fill, the Styx window as the sole directional source — the faint blue-silver luminescence of the river casting long horizontal shadows from the throne across the floor toward camera. Cerberus rendered with the conviction of a real animal of its size — the weight of three lion-scale heads, the musculature of a creature bred from nothing natural, fur matted with the particular dampness of a cave creature, three distinct personalities readable in three distinct resting postures. The obsidian throne's mirror surface reflecting a distorted, cold image of the empty room back on itself. Reference: the architectural world-building of God of War's Helheim, John Martin's vast apocalyptic paintings, the cave compositions of Caspar David Friedrich. 8K resolution, photorealistic render, maximum depth of field across the full room depth, the far Styx window in crisp focus against the near darkness of the entrance."
|
||||||
|
video: "The room is still. The soul-candles burn without movement. The Styx surface shifts faintly through the window, its light shifting across the throne. One of Cerberus's sleeping heads twitches — an ear rotation, the involuntary response of a dreaming animal. Then the not-quite-sleeping head opens fully. All three heads rise simultaneously. All six eyes orient in the same direction: not toward the throne, not toward the river, not toward any visible point in the room. Directly at the camera. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 8.0 MiB |
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
title: "The Masquerade's End"
|
||||||
|
description: "The invitations were sent, the candles were lit, the musicians played, the masks were worn — and one more guest arrived than was invited, in a costume that required no costume, moving through the ballroom with the unhurried patience of something that has attended every party that ever ended this way."
|
||||||
|
date: 2026-05-28T14:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Gothic
|
||||||
|
- Surrealist
|
||||||
|
tags:
|
||||||
|
- Masquerade
|
||||||
|
- Venetian
|
||||||
|
- Edgar Allan Poe
|
||||||
|
- Red Death
|
||||||
|
- Horror
|
||||||
|
- Macabre
|
||||||
|
- Ballroom
|
||||||
|
- Gothic
|
||||||
|
slug: the-masquerades-end
|
||||||
|
prompt:
|
||||||
|
scene: "A wide, sweeping shot of a grand Venetian palazzo ballroom at the precise moment the Red Death moves through it — captured in the surreal, suspended stillness of every other occupant having simultaneously stopped. The ballroom is magnificent: vaulted ceiling forty feet overhead, enormous chandeliers of Murano glass and a thousand candles each, gilded walls hung with enormous oil paintings in ornate frames, a polished marble floor inlaid with a geometric pattern in black and white. The room is filled with perhaps a hundred masquerade guests in costumes of extraordinary richness and fantasy: plague doctors, harlequins, doges, queens, gods, animals, angels, demons, all wearing Venetian masks in every conceivable form — bauta, moretta, volto, colombina — in gilt, in jewel-set porcelain, in feathered black, in painted ivory. Every one of them is frozen mid-motion: a woman mid-turn, her silk skirts arrested in a full spiral that should not hold; a pair of dancers frozen at the peak of a lift; a man mid-laugh with his wine glass raised; a group of four mid-conversation. None of them is looking at what has just entered their midst. Moving through the frozen crowd, touching nothing and no one, is the Red Death: a tall figure in a robe of deep scarlet that has the texture of something organic, wearing a mask that is not a mask — a face of white bone beneath which the eyes are not eyes. On every frozen figure it has already passed, the porcelain mask has developed a single hairline crack. On the nearest figure — a woman in gold — the crack runs from crown to chin."
|
||||||
|
environment: "The palazzo ballroom exists in full Belle Époque Venetian splendour: the ceiling fresco above the chandeliers depicts the gods of Olympus at a feast, the paint still vivid. Along the walls, floor-to-ceiling mirrors in gilt frames multiply the frozen crowd infinitely in both directions — in every reflection, the perspective shifts, and in some of them the Red Death appears in positions it is not occupying in the actual room, as if time moves differently in the glass. At one end of the room, the orchestra is frozen mid-bar: violinists with bows at mid-stroke, the conductor's arm raised and stopped, a cellist leaning forward over the instrument. The music has not ended — it has simply paused, as if the score has a rest that has lasted longer than any rest should. Long tables along the walls hold the untouched supper: crystal, silver, elaborate centrepieces, candelabras, ice sculptures that are still the forms they were carved into, not yet melted. The tall windows behind the orchestra are shuttered in red silk. Through the cracks in the shutters, just visible: Venice at carnival, torchlit, distant, continuing entirely without awareness of what is happening inside."
|
||||||
|
style: "Edgar Allan Poe's The Masque of the Red Death translated into the visual language of Venetian Golden Age painting and Kubrick's compositional control — the palatial grandeur of Tiepolo's ceiling frescoes and Veronese's feast paintings, the formal ceremonial horror of Kubrick's Eyes Wide Shut ballroom sequence, and the supernatural dread of the best Poe adaptation work, all unified in a photorealistic render at maximum atmospheric intensity. The frozen crowd rendered with the hyperreal stasis of a Francis Bacon figure — human form under the extreme pressure of supernatural intervention, fabric and posture locked in positions that physics alone could not maintain. The Red Death as the compositional void around which everything else organises: the eye moves to it despite the overwhelming richness of everything surrounding it, because it is the only figure that is not wrong. The porcelain mask cracks rendered with the precision of a conservation scientist: fine as a spider's thread, catching the candlelight as a white line against the painted porcelain. Lighting: a thousand candles from above as the warm ambient, cold from the mirror reflections as a slightly bluer secondary, the Red Death emitting a near-imperceptible cold light of its own — barely visible, only registering in the shadows it casts. 8K resolution, wide anamorphic lens, deep focus across the full ballroom depth."
|
||||||
|
video: "The candles burn. No music. The frozen crowd maintains every impossible posture without strain. The Red Death moves — slowly, deliberately — between two groups of frozen revellers. As it passes the nearest figure, the hairline crack on her porcelain mask spreads — silently, like frost on glass — from the chin downward across the cheek. The mask does not fall. The Red Death stops. It turns its head — slowly — and faces the camera directly. It has not moved for the rest of the video. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 6.0 MiB |
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: "The Plague Doctor's Round"
|
||||||
|
description: "He makes his rounds at three in the morning, when the city has finally stopped pretending — lantern in one hand, cane in the other, the beak of his mask packed with dried flowers that no longer help anyone, moving through a city that has surrendered to him completely."
|
||||||
|
date: 2026-05-28T12:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Historical
|
||||||
|
tags:
|
||||||
|
- Plague Doctor
|
||||||
|
- Medieval
|
||||||
|
- Fog
|
||||||
|
- Cobblestone
|
||||||
|
- Macabre
|
||||||
|
- Black Death
|
||||||
|
- Night
|
||||||
|
- Atmospheric
|
||||||
|
slug: the-plague-doctors-round
|
||||||
|
prompt:
|
||||||
|
scene: "A lone plague doctor moves through the absolute centre of a fog-drowned medieval street, captured in a long, cinematic shot that places him small against the vast, crushing architecture of a city that has closed around him. He wears the full canonical plague doctor ensemble with hyper-realistic fidelity: a long waxed leather coat the colour of dried blood, black leather gloves to the elbow, a wide-brimmed hat, and the beak mask — long, curved, ominous, the leather cracked and stained from years of use, the eye-pieces dark amber glass that reflects the lantern light without revealing anything within. In his left hand, a wrought iron lantern with a small, steady candle flame — the only warm light source on the street — casting a moving circle of amber barely two metres in any direction. In his right hand, a long wooden cane with a brass tip, which he uses neither for support nor for pointing: he simply carries it, as he always has. His posture is neither hurried nor hesitant. He moves with the particular gait of someone who has been doing this for so long that purpose and resignation have become indistinguishable."
|
||||||
|
environment: "A medieval European city street at three in the morning — a narrow, close-packed canyon of half-timbered buildings leaning toward each other overhead, their upper storeys almost touching, their facades dark and sealed. Every window is shuttered. Every door is closed. On a dozen of those doors, visible as he passes, a red cross — some freshly painted, some weeks old and fading. The cobblestones are wet from recent rain, catching the lantern light in small, irregular reflections. Fog sits low and heavy, dense enough at street level to swallow his feet entirely, reducing him to a coat and hat and beak floating above a white floor. Further down the street — thirty metres, fifty — the fog thickens until the buildings dissolve into grey nothing. A cat sits on a windowsill, watching him pass, absolutely still. At the far edge of visibility, the silhouette of a church spire against a sky that offers no stars and no moon. Somewhere distant, a bell tolling a single note — not the hour, just a bell that has been ringing all night and has not been stopped because there is no one left to stop it."
|
||||||
|
style: "Dutch Golden Age night-scene painting translated into photorealistic cinema — the lantern-lit darkness of Rembrandt's The Night Watch, the claustrophobic medieval streetscapes of Pieter Bruegel the Elder, the fog-choked atmosphere of a Gustave Doré engraving, all rendered with the narrative weight of a Werner Herzog film. Single-source lantern lighting as the entire visual key: warm amber in a tight radius, the rest of the scene in deep, textured darkness ranging from near-black to the diffuse grey-white of the fog layer. The plague doctor rendered with absolute material fidelity: cracked waxed leather, tarnished brass, the slight condensation on the amber glass eyepieces. Colour grade: near-monochrome, dominated by cold greys and blacks, the lantern's amber flame the only warm chromatic event in the entire frame. The cobblestones rendered wet and individually textured, each one reflecting a miniature lantern. 8K resolution, photorealistic render, anamorphic lens with compressed depth perspective emphasising the tunnel-like quality of the street."
|
||||||
|
video: "He walks. The lantern swings slightly in his hand, sending shadows rocking across the wet cobblestones and the building faces. He stops. He tilts the beak mask slightly upward — the posture of listening. Total silence. He does not move for a long moment. Then he continues walking, the lantern swinging again, the shadows rocking. A window shutter on the first floor closes, very slowly, as he passes. He does not look up. Camera holds completely static throughout."
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 2.3 MiB |
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "The Ashen Choir"
|
||||||
|
description: "A masked opera choir stands frozen mid-hymn in a burning gothic theatre, embers drifting through gilded ruin like slow snow as the final note refuses to end."
|
||||||
|
date: 2026-09-13T12:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: false
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Surrealist
|
||||||
|
tags:
|
||||||
|
- Opera
|
||||||
|
- Fire
|
||||||
|
- Masks
|
||||||
|
- Choir
|
||||||
|
- Gothic Theatre
|
||||||
|
- Embers
|
||||||
|
- Macabre
|
||||||
|
slug: the-ashen-choir
|
||||||
|
prompt:
|
||||||
|
scene: "A row of eight robed choir singers stands motionless on the stage of a grand, burning opera house, mouths open mid-note beneath featureless white porcelain masks with delicate gold cracks like veins of gilding. Wide symmetrical shot from the center of the empty stalls, looking directly at the stage. Their robes are charcoal-black velvet trimmed in tarnished gold, hems smoldering at the edges without being consumed. Behind them, the stage curtains burn in slow, silent sheets of orange flame that cast no visible smoke, embers rising instead of falling, drifting upward past the proscenium arch like a reverse snowfall. Sheet music on brass stands in front of each singer curls and blackens at the corners but remains legible. At the center, one singer — taller than the rest — has tilted their masked head back, chin lifted, as if holding an impossibly long final note."
|
||||||
|
environment: "The gilded interior of a 19th-century gothic opera house mid-collapse. Tiered balconies rise on either side, red velvet seats smoldering, gold leaf detailing blistering and curling from the heat. A vast crystal chandelier hangs precariously overhead, half its candles still lit, crystals chiming faintly as the structure shifts. Frescoed ceiling panels depicting weeping muses are scorched at the edges, paint bubbling. Ash and embers drift continuously upward through the auditorium air, catching the footlights' glow. The grand proscenium arch is carved with skeletal cherubs, several of which have cracked and fallen into the orchestra pit below, now empty of musicians."
|
||||||
|
style: "Surrealist gothic theatre horror, painted with the operatic grandeur of a Phantom-of-the-Opera production set ablaze. Rich, saturated palette of deep crimson, black, and molten gold, embers rendered as glowing particulate catching every light source. Reverse-gravity ash and ember physics rendered with total photorealistic conviction despite the impossibility. Hyper-detailed textures: cracked porcelain masks, singed velvet nap, bubbling gold leaf, crystalline chandelier refractions. 8K cinematic render, dramatic underlighting from the footlights, painterly Baroque composition, a stillness that feels suspended mid-collapse rather than static."
|
||||||
|
video: "Embers rise continuously and thickly past the singers in a dense reverse-snowfall, filling the air. The burning curtains behind them ripple and unfurl in constant shifting sheets of flame. The chandelier swings visibly overhead, its crystals chiming and catching the firelight. All eight singers' masked heads tilt further back, shoulders rising and falling visibly as the note builds and holds. Sparks scatter and drift upward continuously from the smoldering seats below. Camera holds static throughout, but everything within the frame moves continuously."
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
title: "The Clockwork Confessor"
|
||||||
|
description: "A towering brass automaton priest hears confession in a gaslit cathedral, its porcelain saint's mask cracked, gears turning beneath tarnished metal as candlelight gutters through the nave."
|
||||||
|
date: 2026-09-13T10:00:00+02:00
|
||||||
|
author: "valknar"
|
||||||
|
featured: true
|
||||||
|
categories:
|
||||||
|
- Dark
|
||||||
|
- Steampunk
|
||||||
|
tags:
|
||||||
|
- Automaton
|
||||||
|
- Clockwork
|
||||||
|
- Cathedral
|
||||||
|
- Confession
|
||||||
|
- Brass
|
||||||
|
- Gothic
|
||||||
|
- Inquisition
|
||||||
|
slug: the-clockwork-confessor
|
||||||
|
prompt:
|
||||||
|
scene: "A towering brass-and-iron automaton priest kneels within an ornate confessional booth of black oak and wrought iron, half-hidden behind a velvet curtain torn at the hem. Full-figure, three-quarter shot from within the confessional's opposite side, looking through the wooden lattice screen. The automaton's body is a masterwork of exposed clockwork: articulated brass ribs housing spinning escapements, a chest cavity where a slow pendulum swings behind smoked glass, tarnished copper tubing running along both arms venting faint puffs of steam with every mechanical breath. Its hands, folded in a gesture of benediction, are long articulated fingers of blackened steel ending in delicate porcelain fingertips. Its face is a cracked porcelain mask shaped into the serene, downturned features of a saint — one long fissure running from brow to jaw, revealing a single glowing amber optic lens beneath. It holds an ancient leather-bound book, pages fluttering slightly though there is no wind. The mask tilts slowly toward the lattice, listening."
|
||||||
|
environment: "The interior of a vast, decaying Gothic cathedral repurposed as a clockwork sanctuary. Soaring ribbed vaults disappear into darkness overhead, hung with rusted gears the size of cartwheels suspended on chains, turning almost imperceptibly in unison with some unseen mechanism. Rows of empty pews stretch into gloom, dust thick on every surface, candlewax pooled in ancient drifts along the aisle. Stained-glass windows depict saints rendered as machines — halos of gear teeth, wings of articulated brass feathers — filtering fractured amber and blue light across the stone floor. A single overturned prayer kneeler lies near the confessional. Somewhere unseen, a great pendulum clock ticks, its sound echoing through the nave like a heartbeat the building cannot stop."
|
||||||
|
style: "Dark steampunk gothic realism. The visual language of Guillermo del Toro's cathedral interiors fused with Jean-Pierre Jeunet's warm-brass mechanical detailing. Chiaroscuro lighting: deep cathedral shadow pierced by shafts of dusty amber candlelight and cold blue stained-glass spill. Hyper-realistic material rendering — oxidised copper, oil-black iron, cracked antique porcelain with fine craquelure, aged velvet with worn nap. 8K photorealistic render, subtle film grain, anamorphic lens flare where light catches brass edges, painterly gothic composition reminiscent of Baroque religious portraiture."
|
||||||
|
video: "The suspended gears overhead rotate visibly and continuously, their brass teeth catching shifting light. Thick steam billows rhythmically from the automaton's neck vents in visible bursts. The automaton visibly turns its head upward toward the gears, then lowers it back to the book, the crack in its mask glinting as it moves. Its brass fingers turn a page of the open book. Candlelight flickers and dances across every surface, casting moving shadows. Camera holds static throughout, but everything within the frame moves continuously."
|
||||||
|
---
|
||||||
@@ -1,69 +1,84 @@
|
|||||||
baseURL = "https://pivoine.art/"
|
baseURL = "https://pivoine.art/"
|
||||||
languageCode = "en-us"
|
locale = "en-US"
|
||||||
title = "Pivoine"
|
title = "Pivoine"
|
||||||
copyright = "© 2026 Pivoine Editorial"
|
copyright = "© 2026 Pivoine Editorial"
|
||||||
|
|
||||||
enableRobotsTXT = true
|
enableRobotsTXT = true
|
||||||
|
|
||||||
[pagination]
|
[pagination]
|
||||||
pagerSize = 12
|
pagerSize = 12
|
||||||
|
|
||||||
[sitemap]
|
[sitemap]
|
||||||
changefreq = "weekly"
|
changefreq = "weekly"
|
||||||
priority = 0.5
|
priority = 0.5
|
||||||
filename = "sitemap.xml"
|
filename = "sitemap.xml"
|
||||||
|
|
||||||
[params]
|
[params]
|
||||||
description = "An editorial magazine for AI-generated art and generative aesthetics."
|
description = "An editorial magazine for AI-generated art and generative aesthetics."
|
||||||
tagline = "Where the machine dreams."
|
tagline = "Where the machine dreams."
|
||||||
author = "Pivoine Editorial"
|
author = "Pivoine Editorial"
|
||||||
ogImage = "/images/og-default.jpg"
|
ogImage = "/images/og-default.jpg"
|
||||||
logoText = "PIVOINE"
|
logoText = "PIVOINE"
|
||||||
copyrightYear = "2026"
|
copyrightYear = "2026"
|
||||||
twitterHandle = "valknar100"
|
twitterHandle = "valknar100"
|
||||||
umamiSrc = "https://umami.pivoine.art/script.js"
|
umamiSrc = "https://umami.pivoine.art/script.js"
|
||||||
umamiId = "9190b814-0480-498a-b5fe-1fe1dc162766"
|
umamiId = "9190b814-0480-498a-b5fe-1fe1dc162766"
|
||||||
|
|
||||||
[params.social]
|
[params.social]
|
||||||
instagram = "https://instagram.com/valknarix"
|
instagram = "https://instagram.com/valknarix"
|
||||||
x = "https://x.com/valknar100"
|
x = "https://x.com/valknar100"
|
||||||
|
|
||||||
[taxonomies]
|
[taxonomies]
|
||||||
category = "categories"
|
category = "categories"
|
||||||
tag = "tags"
|
tag = "tags"
|
||||||
|
|
||||||
[[menus.main]]
|
[[menus.main]]
|
||||||
name = "Posts"
|
name = "Posts"
|
||||||
url = "/posts/"
|
url = "/posts/"
|
||||||
weight = 1
|
weight = 1
|
||||||
[[menus.main]]
|
[[menus.main]]
|
||||||
name = "Authors"
|
name = "Authors"
|
||||||
url = "/authors/"
|
url = "/authors/"
|
||||||
weight = 2
|
weight = 2
|
||||||
[[menus.main]]
|
[[menus.main]]
|
||||||
name = "Categories"
|
name = "Categories"
|
||||||
url = "/categories/"
|
url = "/categories/"
|
||||||
weight = 3
|
weight = 3
|
||||||
[[menus.main]]
|
[[menus.main]]
|
||||||
name = "About"
|
name = "About"
|
||||||
url = "/about/"
|
url = "/about/"
|
||||||
weight = 4
|
weight = 4
|
||||||
|
|
||||||
[[menus.footer]]
|
[[menus.footer]]
|
||||||
name = "About"
|
name = "About"
|
||||||
url = "/about/"
|
url = "/about/"
|
||||||
weight = 1
|
weight = 1
|
||||||
[[menus.footer]]
|
[[menus.footer]]
|
||||||
name = "Imprint"
|
name = "Imprint"
|
||||||
url = "/imprint/"
|
url = "/imprint/"
|
||||||
weight = 2
|
weight = 2
|
||||||
|
[[menus.footer]]
|
||||||
|
name = "Dev"
|
||||||
|
url = "https://dev.pivoine.art"
|
||||||
|
weight = 3
|
||||||
|
[[menus.footer]]
|
||||||
|
name = "Bar"
|
||||||
|
url = "https://bar.pivoine.art"
|
||||||
|
weight = 4
|
||||||
|
[[menus.footer]]
|
||||||
|
name = "Roux"
|
||||||
|
url = "https://roux.pivoine.art"
|
||||||
|
weight = 5
|
||||||
|
|
||||||
[markup.goldmark.renderer]
|
[markup.goldmark.renderer]
|
||||||
unsafe = true
|
unsafe = true
|
||||||
|
|
||||||
[markup.highlight]
|
[markup.highlight]
|
||||||
style = "dracula"
|
style = "dracula"
|
||||||
noClasses = true
|
noClasses = true
|
||||||
|
|
||||||
[build]
|
[build]
|
||||||
writeStats = true
|
writeStats = true
|
||||||
|
|
||||||
|
[security.exec]
|
||||||
|
allow = ['^(dart-)?sass$', '^go$', '^git$', '^node$', '^npx$', '^tailwindcss$']
|
||||||
|
|||||||
@@ -1,65 +1,71 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
<section class="relative min-h-screen flex flex-col items-center justify-center overflow-hidden noise gutter-x py-32 text-center">
|
<section
|
||||||
|
class="relative min-h-screen flex flex-col items-center justify-center overflow-hidden noise gutter-x py-32 text-center"
|
||||||
|
>
|
||||||
|
<!-- Background texture -->
|
||||||
|
<div class="absolute inset-0 speed-lines opacity-60"></div>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0"
|
||||||
|
style="background: radial-gradient(ellipse 80% 60% at 50% 50%, rgba(155,0,255,0.06) 0%, transparent 70%)"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- Background texture -->
|
<!-- Decorative hollow "404" behind everything -->
|
||||||
<div class="absolute inset-0 speed-lines opacity-60"></div>
|
<div class="absolute inset-0 flex items-center justify-center pointer-events-none select-none" aria-hidden="true">
|
||||||
<div class="absolute inset-0"
|
<span
|
||||||
style="background: radial-gradient(ellipse 80% 60% at 50% 50%, rgba(155,0,255,0.06) 0%, transparent 70%)"></div>
|
class="font-display leading-none"
|
||||||
|
style="font-size: clamp(14rem, 40vw, 32rem);
|
||||||
<!-- Decorative hollow "404" behind everything -->
|
|
||||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none select-none" aria-hidden="true">
|
|
||||||
<span
|
|
||||||
class="font-display leading-none"
|
|
||||||
style="font-size: clamp(14rem, 40vw, 32rem);
|
|
||||||
color: transparent;
|
color: transparent;
|
||||||
-webkit-text-stroke: 1px rgba(255,26,140,0.07);
|
-webkit-text-stroke: 1px rgba(255,26,140,0.07);
|
||||||
letter-spacing: -0.02em;">404</span>
|
letter-spacing: -0.02em;"
|
||||||
</div>
|
>404</span
|
||||||
|
>
|
||||||
<!-- Content -->
|
|
||||||
<div class="relative z-10 flex flex-col items-center gap-6 max-w-2xl mx-auto">
|
|
||||||
|
|
||||||
<!-- Eyebrow badge -->
|
|
||||||
<span class="badge badge-gradient" style="animation: mob-link-in 0.5s var(--ease-out-expo) 0ms both">
|
|
||||||
Error 404
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<!-- Main headline -->
|
|
||||||
<h1
|
|
||||||
class="font-display leading-none text-balance"
|
|
||||||
style="font-size: clamp(4rem, 14vw, 10rem);
|
|
||||||
animation: mob-link-in 0.6s var(--ease-out-expo) 80ms both"
|
|
||||||
>
|
|
||||||
<span class="text-stroke-heat">PAGE</span><br>
|
|
||||||
<span class="text-gradient">NOT FOUND</span>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Gradient divider -->
|
|
||||||
<div class="gradient-line w-32 mx-auto" style="animation: mob-link-in 0.4s ease 200ms both"></div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p
|
|
||||||
class="text-fog font-body text-base md:text-lg leading-relaxed max-w-md text-pretty"
|
|
||||||
style="animation: mob-link-in 0.5s var(--ease-out-expo) 260ms both"
|
|
||||||
>
|
|
||||||
This drop got pulled. The page you're looking for doesn't exist, was moved, or got lost in the grid.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- CTA -->
|
|
||||||
<div class="flex flex-wrap gap-4 justify-center"
|
|
||||||
style="animation: mob-link-in 0.5s var(--ease-out-expo) 340ms both">
|
|
||||||
<a href="/" class="btn">Back to the Drop {{ partial "icon.html" "arrow-right" }}</a>
|
|
||||||
<a href="/posts/" class="btn btn-ghost">Browse all posts {{ partial "icon.html" "arrow-right" }}</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- Content -->
|
||||||
|
<div class="relative z-10 flex flex-col items-center gap-6 max-w-2xl mx-auto">
|
||||||
|
<!-- Eyebrow badge -->
|
||||||
|
<span class="badge badge-gradient" style="animation: mob-link-in 0.5s var(--ease-out-expo) 0ms both">
|
||||||
|
Error 404
|
||||||
|
</span>
|
||||||
|
|
||||||
<!-- Bottom graffiti tag -->
|
<!-- Main headline -->
|
||||||
<div
|
<h1
|
||||||
class="graffiti-tag absolute -bottom-6 left-1/2 -translate-x-1/2 select-none pointer-events-none whitespace-nowrap"
|
class="font-display leading-none text-balance"
|
||||||
style="-webkit-text-stroke-color: rgba(0,200,255,0.05);"
|
style="font-size: clamp(4rem, 14vw, 10rem);
|
||||||
aria-hidden="true"
|
animation: mob-link-in 0.6s var(--ease-out-expo) 80ms both"
|
||||||
>LOST</div>
|
>
|
||||||
|
<span class="text-stroke-heat">PAGE</span><br />
|
||||||
|
<span class="text-gradient">NOT FOUND</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
</section>
|
<!-- Gradient divider -->
|
||||||
|
<div class="gradient-line w-32 mx-auto" style="animation: mob-link-in 0.4s ease 200ms both"></div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p
|
||||||
|
class="text-fog font-body text-base md:text-lg leading-relaxed max-w-md text-pretty"
|
||||||
|
style="animation: mob-link-in 0.5s var(--ease-out-expo) 260ms both"
|
||||||
|
>
|
||||||
|
This drop got pulled. The page you're looking for doesn't exist, was moved, or got lost in the grid.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap gap-4 justify-center"
|
||||||
|
style="animation: mob-link-in 0.5s var(--ease-out-expo) 340ms both"
|
||||||
|
>
|
||||||
|
<a href="/" class="btn">Back to the Drop {{ partial "icon.html" "arrow-right" }}</a>
|
||||||
|
<a href="/posts/" class="btn btn-ghost">Browse all posts {{ partial "icon.html" "arrow-right" }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom graffiti tag -->
|
||||||
|
<div
|
||||||
|
class="graffiti-tag absolute -bottom-6 left-1/2 -translate-x-1/2 select-none pointer-events-none whitespace-nowrap"
|
||||||
|
style="-webkit-text-stroke-color: rgba(0,200,255,0.05);"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
LOST
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="{{ .Site.LanguageCode }}" class="scroll-smooth">
|
<html lang="{{ .Site.Language.Locale }}" class="scroll-smooth">
|
||||||
<head>
|
<head>
|
||||||
{{- partial "head.html" . -}}
|
{{- partial "head.html" . -}}
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="bg-void text-paper font-body min-h-screen flex flex-col antialiased"
|
class="bg-void text-paper font-body min-h-screen flex flex-col antialiased"
|
||||||
hx-boost="true"
|
hx-boost="true"
|
||||||
hx-select="#main-content"
|
hx-select="#main-content"
|
||||||
hx-target="#main-content"
|
hx-target="#main-content"
|
||||||
hx-swap="outerHTML"
|
hx-swap="outerHTML"
|
||||||
hx-push-url="true"
|
hx-push-url="true"
|
||||||
>
|
>
|
||||||
|
<!-- HTMX transition progress bar -->
|
||||||
|
<div id="progress-bar" aria-hidden="true"></div>
|
||||||
|
|
||||||
<!-- HTMX transition progress bar -->
|
{{- partial "nav.html" . -}}
|
||||||
<div id="progress-bar" aria-hidden="true"></div>
|
|
||||||
|
|
||||||
{{- partial "nav.html" . -}}
|
{{- block "page-background" . -}}{{- end -}}
|
||||||
|
|
||||||
{{- block "page-background" . -}}{{- end -}}
|
|
||||||
|
|
||||||
<main id="main-content" class="flex-1" hx-history-elt>
|
<main id="main-content" class="flex-1" hx-history-elt>
|
||||||
{{- block "main" . }}{{- end }}
|
{{- block "main" . }}{{- end }}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{{- partial "footer.html" . -}}
|
{{- partial "footer.html" . -}}
|
||||||
|
|
||||||
<!-- ── Global lightbox — lives outside #main-content so HTMX never touches it -->
|
|
||||||
<div
|
<!-- ── Global lightbox — lives outside #main-content so HTMX never touches it -->
|
||||||
x-data="{
|
<div
|
||||||
|
x-data="{
|
||||||
open: false,
|
open: false,
|
||||||
idx: 0,
|
idx: 0,
|
||||||
fill: false,
|
fill: false,
|
||||||
@@ -36,106 +37,141 @@
|
|||||||
prev() { this.idx = (this.idx - 1 + this.items.length) % this.items.length },
|
prev() { this.idx = (this.idx - 1 + this.items.length) % this.items.length },
|
||||||
next() { this.idx = (this.idx + 1) % this.items.length }
|
next() { this.idx = (this.idx + 1) % this.items.length }
|
||||||
}"
|
}"
|
||||||
@lightbox:open.window="items = $event.detail.items; idx = $event.detail.idx; open = true"
|
@lightbox:open.window="items = $event.detail.items; idx = $event.detail.idx; open = true"
|
||||||
@lightbox:close.window="close()"
|
@lightbox:close.window="close()"
|
||||||
@keydown.escape.window="open && close()"
|
@keydown.escape.window="open && close()"
|
||||||
@keydown.arrow-left.window="open && prev()"
|
@keydown.arrow-left.window="open && prev()"
|
||||||
@keydown.arrow-right.window="open && next()"
|
@keydown.arrow-right.window="open && next()"
|
||||||
@keydown.f.window="open && (fill = !fill)"
|
@keydown.f.window="open && (fill = !fill)"
|
||||||
x-show="open"
|
x-show="open"
|
||||||
x-cloak
|
x-cloak
|
||||||
x-transition:enter="transition duration-200 ease-out"
|
x-transition:enter="transition duration-200 ease-out"
|
||||||
x-transition:enter-start="opacity-0"
|
x-transition:enter-start="opacity-0"
|
||||||
x-transition:enter-end="opacity-100"
|
x-transition:enter-end="opacity-100"
|
||||||
x-transition:leave="transition duration-150 ease-in"
|
x-transition:leave="transition duration-150 ease-in"
|
||||||
x-transition:leave-start="opacity-100"
|
x-transition:leave-start="opacity-100"
|
||||||
x-transition:leave-end="opacity-0"
|
x-transition:leave-end="opacity-0"
|
||||||
class="fixed inset-0 z-[200] flex items-center justify-center bg-void/95 backdrop-blur-sm"
|
class="fixed inset-0 z-[200] flex items-center justify-center bg-void/95 backdrop-blur-sm"
|
||||||
@click.self="close()"
|
@click.self="close()"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Media lightbox"
|
aria-label="Media lightbox"
|
||||||
>
|
>
|
||||||
<!-- Top chrome bar -->
|
<!-- Top chrome bar -->
|
||||||
<div class="absolute top-0 inset-x-0 z-10 flex items-center justify-between px-2 h-14"
|
<div
|
||||||
style="background: linear-gradient(to bottom, rgba(5,5,16,0.85), transparent)">
|
class="absolute top-0 inset-x-0 z-10 flex items-center justify-between px-2 h-14"
|
||||||
<div class="absolute top-0 inset-x-0 h-px gradient-line"></div>
|
style="background: linear-gradient(to bottom, rgba(5,5,16,0.85), transparent)"
|
||||||
<div class="w-24"></div>
|
>
|
||||||
<div class="label text-fog tabular-nums"
|
<div class="absolute top-0 inset-x-0 h-px gradient-line"></div>
|
||||||
x-text="items.length ? `${String(idx + 1).padStart(2,'0')} / ${String(items.length).padStart(2,'0')}` : ''"></div>
|
<div class="w-24"></div>
|
||||||
<div class="flex items-center w-24 justify-end gap-1">
|
<div
|
||||||
<button @click="fill = !fill"
|
class="label text-fog tabular-nums"
|
||||||
class="w-10 h-10 flex items-center justify-center label transition-colors"
|
x-text="items.length ? `${String(idx + 1).padStart(2,'0')} / ${String(items.length).padStart(2,'0')}` : ''"
|
||||||
:class="fill ? 'text-heat hover:text-chalk' : 'text-fog hover:text-heat'"
|
></div>
|
||||||
:aria-label="fill ? 'Switch to fit mode' : 'Switch to fill mode'"
|
<div class="flex items-center w-24 justify-end gap-1">
|
||||||
x-text="fill ? 'FIT' : 'FILL'"></button>
|
<button
|
||||||
<button @click="close()"
|
@click="fill = !fill"
|
||||||
class="w-10 h-10 flex items-center justify-center label text-fog hover:text-chalk transition-colors"
|
class="w-10 h-10 flex items-center justify-center label transition-colors"
|
||||||
aria-label="Close">✕</button>
|
: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>
|
||||||
|
<!-- Prev -->
|
||||||
|
<button
|
||||||
|
@click="prev()"
|
||||||
|
x-show="items.length > 1"
|
||||||
|
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"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
{{- partial "icon.html" "arrow-left" -}}
|
||||||
|
</button>
|
||||||
|
<!-- Media -->
|
||||||
|
<div
|
||||||
|
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'"
|
||||||
|
>
|
||||||
|
<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() }"
|
||||||
|
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
||||||
|
class="transition-all duration-300"
|
||||||
|
controls
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
></video>
|
||||||
|
<img
|
||||||
|
x-show="items[idx] && !items[idx].video"
|
||||||
|
:src="items[idx] && !items[idx].video ? items[idx].img : ''"
|
||||||
|
alt=""
|
||||||
|
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
||||||
|
class="transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- Next -->
|
||||||
|
<button
|
||||||
|
@click="next()"
|
||||||
|
x-show="items.length > 1"
|
||||||
|
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"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
{{- partial "icon.html" "arrow-right" -}}
|
||||||
|
</button>
|
||||||
|
<!-- 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>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Prev -->
|
|
||||||
<button @click="prev()" x-show="items.length > 1"
|
|
||||||
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"
|
|
||||||
aria-label="Previous">{{- partial "icon.html" "arrow-left" -}}</button>
|
|
||||||
<!-- Media -->
|
|
||||||
<div 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'">
|
|
||||||
<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() }"
|
|
||||||
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
|
||||||
class="transition-all duration-300"
|
|
||||||
controls loop muted playsinline></video>
|
|
||||||
<img
|
|
||||||
x-show="items[idx] && !items[idx].video"
|
|
||||||
:src="items[idx] && !items[idx].video ? items[idx].img : ''"
|
|
||||||
alt=""
|
|
||||||
:class="fill ? 'w-full h-full object-cover' : 'max-w-full max-h-full object-contain'"
|
|
||||||
class="transition-all duration-300">
|
|
||||||
</div>
|
|
||||||
<!-- Next -->
|
|
||||||
<button @click="next()" x-show="items.length > 1"
|
|
||||||
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"
|
|
||||||
aria-label="Next">{{- partial "icon.html" "arrow-right" -}}</button>
|
|
||||||
<!-- 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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- HTMX (page transitions & progressive enhancement) -->
|
<!-- HTMX (page transitions & progressive enhancement) -->
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"></script>
|
||||||
<!-- Alpine.js store initialisation (must run before alpine:init) -->
|
<!-- Alpine.js store initialisation (must run before alpine:init) -->
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener("alpine:init", () => {
|
||||||
Alpine.store('nav', { path: window.location.pathname, open: false })
|
Alpine.store("nav", { path: window.location.pathname, open: false });
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
<!-- Alpine.js (reactive UI — nav, interactions) -->
|
<!-- Alpine.js (reactive UI — nav, interactions) -->
|
||||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script>
|
||||||
<!-- Site JS -->
|
<!-- Site JS -->
|
||||||
{{- $js := resources.Get "js/main.js" -}}
|
{{- $js := resources.Get "js/main.js" -}}
|
||||||
{{- if eq hugo.Environment "production" -}}
|
{{- if eq hugo.Environment "production" -}}
|
||||||
{{- $js = $js | minify | fingerprint "sha256" -}}
|
{{- $js = $js | minify | fingerprint "sha256" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<script src="{{ $js.RelPermalink }}"{{ if eq hugo.Environment "production" }} integrity="{{ $js.Data.Integrity }}"{{ end }} defer></script>
|
<script
|
||||||
|
src="{{ $js.RelPermalink }}"
|
||||||
|
{{ if eq hugo.Environment "production" }}integrity="{{ $js.Data.Integrity }}"{{ end }}
|
||||||
|
defer
|
||||||
|
></script>
|
||||||
|
|
||||||
{{- block "scripts" . }}{{- end }}
|
{{- block "scripts" . }}{{- end }}
|
||||||
|
|
||||||
{{- if and (eq hugo.Environment "production") .Site.Params.umamiId -}}
|
{{- if and (eq hugo.Environment "production") .Site.Params.umamiId -}}
|
||||||
<!-- Umami analytics — cookie-free, self-hosted -->
|
<!-- Umami analytics — cookie-free, self-hosted -->
|
||||||
<script defer src="{{ .Site.Params.umamiSrc }}" data-website-id="{{ .Site.Params.umamiId }}"></script>
|
<script defer src="{{ .Site.Params.umamiSrc }}" data-website-id="{{ .Site.Params.umamiId }}"></script>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,34 +1,32 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
<!-- Page header -->
|
||||||
|
<header class="gutter-x pt-36 pb-14 border-b border-zinc speed-lines">
|
||||||
|
<div class="max-w-4xl">
|
||||||
|
{{- with .Data.Singular -}}
|
||||||
|
<span class="badge badge-pulse mb-5 inline-block">{{ . | title }}</span>
|
||||||
|
{{- else -}}
|
||||||
|
<span class="badge badge-pulse mb-5 inline-block">{{ .Kind | title }}</span>
|
||||||
|
{{- end -}}
|
||||||
|
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance leading-none">{{ .Title }}</h1>
|
||||||
|
{{- with .Description -}}
|
||||||
|
<p class="text-chalk text-lg mt-5 max-w-xl leading-relaxed text-pretty font-body">{{ . }}</p>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Page header -->
|
<div class="gradient-line"></div>
|
||||||
<header class="gutter-x pt-36 pb-14 border-b border-zinc speed-lines">
|
|
||||||
<div class="max-w-4xl">
|
<!-- Posts grid -->
|
||||||
{{- with .Data.Singular -}}
|
<section class="gutter-x py-16 md:py-24">
|
||||||
<span class="badge badge-pulse mb-5 inline-block">{{ . | title }}</span>
|
{{- if .Paginator.Pages -}}
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||||
|
{{- range .Paginator.Pages -}}
|
||||||
|
{{- partial "post-card.html" . -}}
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
{{- partial "pagination.html" . -}}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
<span class="badge badge-pulse mb-5 inline-block">{{ .Kind | title }}</span>
|
<p class="label text-fog text-center py-24">No posts yet.</p>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance leading-none">{{ .Title }}</h1>
|
</section>
|
||||||
{{- with .Description -}}
|
|
||||||
<p class="text-chalk text-lg mt-5 max-w-xl leading-relaxed text-pretty font-body">{{ . }}</p>
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="gradient-line"></div>
|
|
||||||
|
|
||||||
<!-- Posts grid -->
|
|
||||||
<section class="gutter-x py-16 md:py-24">
|
|
||||||
{{- if .Paginator.Pages -}}
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
|
||||||
{{- range .Paginator.Pages -}}
|
|
||||||
{{- partial "post-card.html" . -}}
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
{{- partial "pagination.html" . -}}
|
|
||||||
{{- else -}}
|
|
||||||
<p class="label text-fog text-center py-24">No posts yet.</p>
|
|
||||||
{{- end -}}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
<!-- Page header -->
|
||||||
|
<header class="gutter-x pt-36 pb-16 border-b border-mist">
|
||||||
|
<div class="max-w-2xl">
|
||||||
|
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance mb-5">{{ .Title }}</h1>
|
||||||
|
{{- with .Description -}}
|
||||||
|
<p class="text-chalk text-xl leading-relaxed text-pretty">{{ . }}</p>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Page header -->
|
<!-- Page content -->
|
||||||
<header class="gutter-x pt-36 pb-16 border-b border-mist">
|
<article class="gutter-x py-16 md:py-24 max-w-3xl prose-editorial">
|
||||||
<div class="max-w-3xl">
|
{{ .Content }}
|
||||||
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance mb-5">{{ .Title }}</h1>
|
</article>
|
||||||
{{- with .Description -}}
|
|
||||||
<p class="text-chalk text-xl leading-relaxed text-pretty">{{ . }}</p>
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Page content -->
|
|
||||||
<article class="gutter-x py-16 md:py-24 max-w-3xl prose-editorial">
|
|
||||||
{{ .Content }}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
<!-- Taxonomy header -->
|
||||||
|
<header class="gutter-x pt-36 pb-14 border-b border-zinc speed-lines">
|
||||||
|
<div class="max-w-4xl">
|
||||||
|
<span class="badge badge-frost text-void mb-5 inline-block">Browse</span>
|
||||||
|
<h1 class="font-display text-5xl md:text-7xl text-paper leading-none">{{ .Title }}</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Taxonomy header -->
|
<div class="gradient-line"></div>
|
||||||
<header class="gutter-x pt-36 pb-14 border-b border-zinc speed-lines">
|
|
||||||
<div class="max-w-4xl">
|
|
||||||
<span class="badge badge-frost text-void mb-5 inline-block">Browse</span>
|
|
||||||
<h1 class="font-display text-5xl md:text-7xl text-paper leading-none">{{ .Title }}</h1>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="gradient-line"></div>
|
<!-- Terms badge cloud -->
|
||||||
|
<section class="gutter-x py-16 md:py-24">
|
||||||
<!-- Terms badge cloud -->
|
{{- if .Data.Terms -}}
|
||||||
<section class="gutter-x py-16 md:py-24">
|
<div class="flex flex-wrap gap-4">
|
||||||
{{- if .Data.Terms -}}
|
{{- range .Data.Terms.Alphabetical -}}
|
||||||
<div class="flex flex-wrap gap-4">
|
<a
|
||||||
{{- range .Data.Terms.Alphabetical -}}
|
href="{{ .Page.RelPermalink }}"
|
||||||
<a
|
class="group card-comic flex items-baseline gap-3 bg-concrete px-5 py-4 hover:border-heat transition-all"
|
||||||
href="{{ .Page.RelPermalink }}"
|
>
|
||||||
class="group card-comic flex items-baseline gap-3 bg-concrete px-5 py-4 hover:border-heat transition-all"
|
<span class="font-display text-2xl text-paper group-hover:text-heat transition-colors leading-none"
|
||||||
>
|
>{{ .Page.Title }}</span
|
||||||
<span class="font-display text-2xl text-paper group-hover:text-heat transition-colors leading-none">{{ .Page.Title }}</span>
|
>
|
||||||
<span class="badge badge-outline text-xs ml-1 group-hover:bg-heat group-hover:text-white group-hover:border-heat transition-all">{{ .Count }}</span>
|
<span
|
||||||
</a>
|
class="badge badge-outline text-xs ml-1 group-hover:bg-heat group-hover:text-white group-hover:border-heat transition-all"
|
||||||
|
>{{ .Count }}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
{{- else -}}
|
||||||
|
<p class="label text-fog text-center py-24">No entries yet.</p>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</section>
|
||||||
{{- else -}}
|
|
||||||
<p class="label text-fog text-center py-24">No entries yet.</p>
|
|
||||||
{{- end -}}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,54 +1,59 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="gutter-x pt-36 pb-14 border-b border-zinc speed-lines">
|
||||||
|
<span class="badge badge-frost text-void mb-5 inline-block">Contributors</span>
|
||||||
|
<h1 class="font-display text-5xl md:text-7xl text-paper leading-none">Authors</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Header -->
|
<div class="gradient-line"></div>
|
||||||
<header class="gutter-x pt-36 pb-14 border-b border-zinc speed-lines">
|
|
||||||
<span class="badge badge-frost text-void mb-5 inline-block">Contributors</span>
|
|
||||||
<h1 class="font-display text-5xl md:text-7xl text-paper leading-none">Authors</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="gradient-line"></div>
|
<!-- Authors grid -->
|
||||||
|
<section class="gutter-x py-16 md:py-24">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{{- range .Pages -}}
|
||||||
|
<a href="{{ .RelPermalink }}" class="group card-comic flex flex-col items-start gap-5 p-6 bg-concrete">
|
||||||
|
<!-- Avatar — bundle avatar.* first, fall back to .Params.avatar -->
|
||||||
|
{{- $avatarRes := .Resources.GetMatch "avatar.*" -}}
|
||||||
|
{{- $avatarSrc := "" -}}
|
||||||
|
{{- with $avatarRes }}
|
||||||
|
{{ $avatarSrc = .RelPermalink }}
|
||||||
|
{{ else }}
|
||||||
|
{{ with $.Params.avatar }}{{ $avatarSrc = . }}{{ end }}
|
||||||
|
{{ end -}}
|
||||||
|
{{- if $avatarSrc -}}
|
||||||
|
<div class="w-20 h-20 overflow-hidden border border-zinc group-hover:border-heat transition-colors">
|
||||||
|
<img
|
||||||
|
src="{{ $avatarSrc }}"
|
||||||
|
alt="{{ .Params.name | default .Title }}"
|
||||||
|
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{{- else -}}
|
||||||
|
<div class="w-20 h-20 bg-zinc border border-zinc flex items-center justify-center">
|
||||||
|
<span class="font-display text-2xl text-fog">{{ substr (.Params.name | default .Title) 0 1 }}</span>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
<!-- Authors grid -->
|
|
||||||
<section class="gutter-x py-16 md:py-24">
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{{- range .Pages -}}
|
|
||||||
<a href="{{ .RelPermalink }}" class="group card-comic flex flex-col items-start gap-5 p-6 bg-concrete">
|
|
||||||
|
|
||||||
<!-- Avatar — bundle avatar.* first, fall back to .Params.avatar -->
|
<!-- Info -->
|
||||||
{{- $avatarRes := .Resources.GetMatch "avatar.*" -}}
|
<div class="flex-1">
|
||||||
{{- $avatarSrc := "" -}}
|
<h2 class="font-display text-xl text-paper group-hover:text-heat transition-colors mb-2 leading-none">
|
||||||
{{- with $avatarRes }}{{ $avatarSrc = .RelPermalink }}{{ else }}{{ with $.Params.avatar }}{{ $avatarSrc = . }}{{ end }}{{ end -}}
|
{{ .Params.name | default .Title }}
|
||||||
{{- if $avatarSrc -}}
|
</h2>
|
||||||
<div class="w-20 h-20 overflow-hidden border border-zinc group-hover:border-heat transition-colors">
|
{{- with .Params.bio -}}
|
||||||
<img
|
<p class="text-fog text-sm leading-relaxed line-clamp-3 mt-2 font-body">{{ . }}</p>
|
||||||
src="{{ $avatarSrc }}"
|
{{- end -}}
|
||||||
alt="{{ .Params.name | default .Title }}"
|
</div>
|
||||||
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
|
|
||||||
loading="lazy"
|
<span
|
||||||
>
|
class="badge badge-outline group-hover:bg-heat group-hover:text-white group-hover:border-heat transition-all mt-auto"
|
||||||
</div>
|
>
|
||||||
{{- else -}}
|
View works
|
||||||
<div class="w-20 h-20 bg-zinc border border-zinc flex items-center justify-center">
|
{{ partial "icon.html" "arrow-right" }}
|
||||||
<span class="font-display text-2xl text-fog">{{ substr (.Params.name | default .Title) 0 1 }}</span>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
</div>
|
||||||
<!-- Info -->
|
</section>
|
||||||
<div class="flex-1">
|
|
||||||
<h2 class="font-display text-xl text-paper group-hover:text-heat transition-colors mb-2 leading-none">
|
|
||||||
{{ .Params.name | default .Title }}
|
|
||||||
</h2>
|
|
||||||
{{- with .Params.bio -}}
|
|
||||||
<p class="text-fog text-sm leading-relaxed line-clamp-3 mt-2 font-body">{{ . }}</p>
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="badge badge-outline group-hover:bg-heat group-hover:text-white group-hover:border-heat transition-all mt-auto">
|
|
||||||
View works {{ partial "icon.html" "arrow-right" }}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,97 +1,105 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
{{- $slug := .File.Dir | strings.TrimSuffix "/" | path.Base -}}
|
{{- $slug := .File.Dir | strings.TrimSuffix "/" | path.Base -}}
|
||||||
{{- $posts := where .Site.RegularPages "Params.author" $slug -}}
|
{{- $posts := where .Site.RegularPages "Params.author" $slug -}}
|
||||||
|
|
||||||
<!-- ── PROFILE HEADER ────────────────────────────────────────── -->
|
|
||||||
<header class="gutter-x pt-36 pb-20 border-b border-zinc speed-lines">
|
|
||||||
<div class="max-w-5xl mx-auto">
|
|
||||||
<div class="flex flex-col md:flex-row gap-10 md:gap-16 items-start">
|
|
||||||
|
|
||||||
<!-- Avatar — page bundle avatar.* takes precedence, falls back to .Params.avatar -->
|
<!-- ── PROFILE HEADER ────────────────────────────────────────── -->
|
||||||
{{- $avatarRes := .Resources.GetMatch "avatar.*" -}}
|
<header class="gutter-x pt-36 pb-20 border-b border-zinc speed-lines">
|
||||||
{{- $avatarSrc := "" -}}
|
<div class="max-w-5xl mx-auto">
|
||||||
{{- with $avatarRes }}{{ $avatarSrc = .RelPermalink }}{{ else }}{{ with $.Params.avatar }}{{ $avatarSrc = . }}{{ end }}{{ end -}}
|
<div class="flex flex-col md:flex-row gap-10 md:gap-16 items-start">
|
||||||
{{- if $avatarSrc -}}
|
<!-- Avatar — page bundle avatar.* takes precedence, falls back to .Params.avatar -->
|
||||||
<div class="flex-shrink-0 w-36 h-36 md:w-48 md:h-48 overflow-hidden border-2 border-heat neon-heat">
|
{{- $avatarRes := .Resources.GetMatch "avatar.*" -}}
|
||||||
<img
|
{{- $avatarSrc := "" -}}
|
||||||
src="{{ $avatarSrc }}"
|
{{- with $avatarRes }}
|
||||||
alt="{{ .Params.name | default .Title }}"
|
{{ $avatarSrc = .RelPermalink }}
|
||||||
class="w-full h-full object-cover"
|
{{ else }}
|
||||||
loading="lazy"
|
{{ with $.Params.avatar }}{{ $avatarSrc = . }}{{ end }}
|
||||||
>
|
{{ end -}}
|
||||||
</div>
|
{{- if $avatarSrc -}}
|
||||||
{{- else -}}
|
<div class="flex-shrink-0 w-36 h-36 md:w-48 md:h-48 overflow-hidden border-2 border-heat neon-heat">
|
||||||
<div class="flex-shrink-0 w-36 h-36 md:w-48 md:h-48 bg-concrete border-2 border-zinc flex items-center justify-center">
|
<img
|
||||||
<span class="font-display text-5xl text-smoke">{{ substr (.Params.name | default .Title) 0 1 }}</span>
|
src="{{ $avatarSrc }}"
|
||||||
</div>
|
alt="{{ .Params.name | default .Title }}"
|
||||||
{{- end -}}
|
class="w-full h-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
<!-- Info -->
|
/>
|
||||||
<div class="flex-1 min-w-0">
|
</div>
|
||||||
<span class="badge badge-gradient mb-5 inline-block">Artist</span>
|
{{- else -}}
|
||||||
<h1 class="font-display text-4xl md:text-6xl text-paper text-balance mb-4 leading-none">
|
<div
|
||||||
{{ .Params.name | default .Title }}
|
class="flex-shrink-0 w-36 h-36 md:w-48 md:h-48 bg-concrete border-2 border-zinc flex items-center justify-center"
|
||||||
</h1>
|
>
|
||||||
{{- with .Params.bio -}}
|
<span class="font-display text-5xl text-smoke">{{ substr (.Params.name | default .Title) 0 1 }}</span>
|
||||||
<p class="text-chalk text-lg max-w-2xl leading-relaxed text-pretty mb-8 font-body font-light">{{ . }}</p>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
<!-- Social links — values may be full URLs or bare slugs -->
|
|
||||||
{{- with .Params.social -}}
|
<!-- Info -->
|
||||||
{{- $bases := dict "instagram" "https://instagram.com/" "x" "https://x.com/" "twitter" "https://x.com/" "github" "https://github.com/" "behance" "https://www.behance.net/" "artstation" "https://www.artstation.com/" -}}
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex flex-wrap gap-3">
|
<span class="badge badge-gradient mb-5 inline-block">Artist</span>
|
||||||
{{- range $platform, $handle := . -}}
|
<h1 class="font-display text-4xl md:text-6xl text-paper text-balance mb-4 leading-none">
|
||||||
{{- $href := cond (hasPrefix $handle "http") $handle (printf "%s%s" (index $bases $platform | default "#") $handle) -}}
|
{{ .Params.name | default .Title }}
|
||||||
<a
|
</h1>
|
||||||
href="{{ $href }}"
|
{{- with .Params.bio -}}
|
||||||
target="_blank"
|
<p class="text-chalk text-lg max-w-2xl leading-relaxed text-pretty mb-8 font-body font-light">{{ . }}</p>
|
||||||
rel="noopener noreferrer"
|
{{- end -}}
|
||||||
class="badge badge-outline"
|
|
||||||
>{{ $platform | upper }} {{ partial "icon.html" "arrow-external" }}</a>
|
|
||||||
|
<!-- Social links — values may be full URLs or bare slugs -->
|
||||||
|
{{- with .Params.social -}}
|
||||||
|
{{- $bases := dict "instagram" "https://instagram.com/" "x" "https://x.com/" "twitter" "https://x.com/" "github" "https://github.com/" "behance" "https://www.behance.net/" "artstation" "https://www.artstation.com/" -}}
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
{{- range $platform, $handle := . -}}
|
||||||
|
{{- $href := cond (hasPrefix $handle "http") $handle (printf "%s%s" (index $bases $platform | default "#") $handle) -}}
|
||||||
|
<a href="{{ $href }}" target="_blank" rel="noopener noreferrer" class="badge badge-outline"
|
||||||
|
>{{ $platform | upper }} {{ partial "icon.html" "arrow-external" }}</a
|
||||||
|
>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Post count -->
|
||||||
|
{{- if $posts -}}
|
||||||
|
<p class="label text-fog mt-6">
|
||||||
|
{{ len $posts }} {{ if eq (len $posts) 1 }}editorial{{ else }}editorials{{ end }}
|
||||||
|
</p>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
<!-- Post count -->
|
|
||||||
{{- if $posts -}}
|
|
||||||
<p class="label text-fog mt-6">{{ len $posts }} {{ if eq (len $posts) 1 }}editorial{{ else }}editorials{{ end }}</p>
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="gradient-line"></div>
|
<div class="gradient-line"></div>
|
||||||
|
|
||||||
<!-- ── AUTHOR BIO (Markdown body) ────────────────────────────── -->
|
<!-- ── AUTHOR BIO (Markdown body) ────────────────────────────── -->
|
||||||
{{- with .Content -}}
|
{{- with .Content -}}
|
||||||
<div class="gutter-x py-12 border-b border-zinc">
|
<div class="gutter-x py-12 border-b border-zinc">
|
||||||
<div class="max-w-5xl mx-auto prose-editorial">{{ . }}</div>
|
<div class="max-w-5xl mx-auto prose-editorial">{{ . }}</div>
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
<!-- ── AUTHOR'S POSTS ─────────────────────────────────────────── -->
|
|
||||||
{{- if $posts -}}
|
<!-- ── AUTHOR'S POSTS ─────────────────────────────────────────── -->
|
||||||
<section class="gutter-x py-16 md:py-24">
|
{{- if $posts -}}
|
||||||
<div class="max-w-5xl mx-auto">
|
<section class="gutter-x py-16 md:py-24">
|
||||||
<div class="flex items-center gap-5 mb-10">
|
<div class="max-w-5xl mx-auto">
|
||||||
<h2 class="font-display text-3xl md:text-5xl text-paper leading-none">
|
<div class="flex items-center gap-5 mb-10">
|
||||||
Works by <span class="text-gradient">{{ .Params.name | default .Title }}</span>
|
<h2 class="font-display text-3xl md:text-5xl text-paper leading-none">
|
||||||
</h2>
|
Works by <span class="text-gradient">{{ .Params.name | default .Title }}</span>
|
||||||
<div class="flex-1 gradient-line ml-2"></div>
|
</h2>
|
||||||
</div>
|
<div class="flex-1 gradient-line ml-2"></div>
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
</div>
|
||||||
{{- range first 4 ($posts.ByDate.Reverse) -}}
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||||
{{- partial "post-card.html" . -}}
|
{{- range first 4 ($posts.ByDate.Reverse) -}}
|
||||||
{{- end -}}
|
{{- partial "post-card.html" . -}}
|
||||||
</div>
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</div>
|
||||||
{{- else -}}
|
</section>
|
||||||
<div class="gutter-x py-24 text-center">
|
{{- else -}}
|
||||||
<p class="label text-fog">No editorials published yet.</p>
|
<div class="gutter-x py-24 text-center">
|
||||||
</div>
|
<p class="label text-fog">No editorials published yet.</p>
|
||||||
{{- end -}}
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
<!-- Category header -->
|
||||||
|
<header class="gutter-x pt-36 pb-16 border-b border-mist">
|
||||||
|
<div class="max-w-4xl">
|
||||||
|
<span class="label text-ash block mb-4">Category</span>
|
||||||
|
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance">{{ .Title }}</h1>
|
||||||
|
<p class="text-ash label mt-5">
|
||||||
|
{{ len .Pages }} {{ if eq (len .Pages) 1 }}editorial{{ else }}editorials{{ end }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Category header -->
|
<!-- Posts grid -->
|
||||||
<header class="gutter-x pt-36 pb-16 border-b border-mist">
|
<section class="gutter-x py-16 md:py-24">
|
||||||
<div class="max-w-4xl">
|
{{- if .Paginator.Pages -}}
|
||||||
<span class="label text-ash block mb-4">Category</span>
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-5 gap-y-12">
|
||||||
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance">{{ .Title }}</h1>
|
{{- range .Paginator.Pages -}}
|
||||||
<p class="text-ash label mt-5">{{ len .Pages }} {{ if eq (len .Pages) 1 }}editorial{{ else }}editorials{{ end }}</p>
|
{{- partial "post-card.html" . -}}
|
||||||
</div>
|
{{- end -}}
|
||||||
</header>
|
</div>
|
||||||
|
{{- partial "pagination.html" . -}}
|
||||||
<!-- Posts grid -->
|
{{- else -}}
|
||||||
<section class="gutter-x py-16 md:py-24">
|
<p class="text-ash label text-center py-24">No posts in this category yet.</p>
|
||||||
{{- if .Paginator.Pages -}}
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-5 gap-y-12">
|
|
||||||
{{- range .Paginator.Pages -}}
|
|
||||||
{{- partial "post-card.html" . -}}
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</section>
|
||||||
{{- partial "pagination.html" . -}}
|
|
||||||
{{- else -}}
|
|
||||||
<p class="text-ash label text-center py-24">No posts in this category yet.</p>
|
|
||||||
{{- end -}}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,134 +1,158 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
{{- $posts := where .Site.RegularPages "Section" "posts" -}}
|
{{- $posts := where .Site.RegularPages "Section" "posts" -}}
|
||||||
{{- $featured := where $posts "Params.featured" true -}}
|
{{- $featured := where $posts "Params.featured" true -}}
|
||||||
{{- $hero := index $featured 0 -}}
|
{{- $hero := index $featured 0 -}}
|
||||||
{{- $latest := where $posts "Permalink" "ne" $hero.Permalink -}}
|
{{- $latest := where $posts "Permalink" "ne" $hero.Permalink -}}
|
||||||
|
|
||||||
<!-- ── HERO ──────────────────────────────────────────────────── -->
|
|
||||||
{{- with $hero -}}
|
|
||||||
<section class="relative min-h-screen flex items-end overflow-hidden noise">
|
|
||||||
|
|
||||||
<!-- Background media — bundle banner.png first, fallback to front matter -->
|
<!-- ── HERO ──────────────────────────────────────────────────── -->
|
||||||
{{- $heroBanner := .Resources.GetMatch "banner.png" -}}
|
{{- with $hero -}}
|
||||||
{{- if not $heroBanner -}}{{- $heroBanner = .Resources.GetMatch "banner.*" -}}{{- end -}}
|
<section class="relative min-h-screen flex items-end overflow-hidden noise">
|
||||||
{{- if $heroBanner -}}
|
<!-- Background media — bundle banner.png → 01.png → front matter -->
|
||||||
<div class="absolute inset-0">
|
{{- $heroBanner := .Resources.GetMatch "banner.png" -}}
|
||||||
{{- partial "img.html" (dict "res" $heroBanner "widths" (slice 1200 1920 2560) "sizes" "100vw" "class" "w-full h-full object-cover" "alt" "" "loading" "eager") -}}
|
{{- if not $heroBanner -}}{{- $heroBanner = .Resources.GetMatch "01.png" -}}{{- end -}}
|
||||||
</div>
|
{{- if $heroBanner -}}
|
||||||
{{- else -}}
|
<div class="absolute inset-0">
|
||||||
{{- $heroBannerSrc := "" -}}
|
{{- partial "img.html" (dict "res" $heroBanner "widths" (slice 1200 1920 2560) "sizes" "100vw" "class" "w-full h-full object-cover" "alt" "" "loading" "eager") -}}
|
||||||
{{- with .Params.background }}{{ $heroBannerSrc = .src }}{{ else }}{{ with $.Params.banner }}{{ $heroBannerSrc = .src }}{{ end }}{{ end -}}
|
</div>
|
||||||
{{- with $heroBannerSrc -}}
|
{{- else -}}
|
||||||
<div class="absolute inset-0">
|
{{- $heroBannerSrc := "" -}}
|
||||||
<img src="{{ . }}" alt="" class="w-full h-full object-cover" loading="eager" decoding="async">
|
{{- with .Params.background }}
|
||||||
</div>
|
{{ $heroBannerSrc = .src }}
|
||||||
{{- end -}}
|
{{ else }}
|
||||||
{{- end -}}
|
{{ with $.Params.banner }}{{ $heroBannerSrc = .src }}{{ end }}
|
||||||
|
{{ end -}}
|
||||||
<!-- Cinematic overlay -->
|
{{- with $heroBannerSrc -}}
|
||||||
<div class="absolute inset-0 overlay-gradient-hero"></div>
|
<div class="absolute inset-0">
|
||||||
|
<img src="{{ . }}" alt="" class="w-full h-full object-cover" loading="eager" decoding="async" />
|
||||||
<!-- Hero content -->
|
|
||||||
<div class="relative z-10 gutter-x pb-20 md:pb-32 w-full">
|
|
||||||
<div class="max-w-5xl">
|
|
||||||
|
|
||||||
<!-- Drop label + category badges -->
|
|
||||||
<div class="flex flex-wrap items-center gap-3 mb-6">
|
|
||||||
<span class="badge badge-pulse">Featured Drop</span>
|
|
||||||
{{- with .Params.categories -}}
|
|
||||||
{{- range first 1 . -}}
|
|
||||||
<a href="/categories/{{ . | urlize }}/" class="badge badge-outline">{{ . }}</a>
|
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<h1 class="font-display text-6xl md:text-8xl text-paper text-balance mb-6 leading-none">
|
|
||||||
{{- .Title -}}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p class="text-chalk text-base md:text-lg max-w-2xl mb-10 leading-relaxed text-pretty font-body opacity-90 line-clamp-3">
|
|
||||||
{{- .Description -}}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- CTA + author -->
|
|
||||||
<div class="flex flex-wrap items-center gap-6">
|
|
||||||
<a href="{{ .RelPermalink }}" class="btn btn-outline">View Editorial {{ partial "icon.html" "arrow-right" }}</a>
|
|
||||||
{{- with .Params.author -}}
|
|
||||||
{{- $author := site.GetPage (printf "authors/%s" .) -}}
|
|
||||||
{{- with $author -}}
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
{{- $avRes := .Resources.GetMatch "avatar.*" -}}
|
|
||||||
{{- $avSrc := "" -}}
|
|
||||||
{{- with $avRes }}{{ $avSrc = .RelPermalink }}{{ else }}{{ with $author.Params.avatar }}{{ $avSrc = . }}{{ end }}{{ end -}}
|
|
||||||
{{- with $avSrc -}}
|
|
||||||
<img src="{{ . }}" alt="{{ $author.Params.name | default $author.Title }}"
|
|
||||||
class="w-8 h-8 object-cover border border-heat/50">
|
|
||||||
{{- end -}}
|
|
||||||
<span class="label text-chalk">
|
|
||||||
By <a href="{{ .RelPermalink }}" class="text-heat hover:text-frost transition-colors">{{ .Params.name | default .Title }}</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Cinematic overlay -->
|
||||||
|
<div class="absolute inset-0 overlay-gradient-hero"></div>
|
||||||
|
|
||||||
|
<!-- Hero content -->
|
||||||
|
<div class="relative z-10 gutter-x pb-20 md:pb-32 w-full">
|
||||||
|
<div class="max-w-5xl">
|
||||||
|
<!-- Drop label + category badges -->
|
||||||
|
<div class="flex flex-wrap items-center gap-3 mb-6">
|
||||||
|
<span class="badge badge-pulse">Featured Drop</span>
|
||||||
|
{{- with .Params.categories -}}
|
||||||
|
{{- range . -}}
|
||||||
|
<a href="/categories/{{ . | urlize }}/" class="badge badge-outline">{{ . }}</a>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h1 class="font-display text-6xl md:text-8xl text-paper text-balance mb-6 leading-none">
|
||||||
|
{{- .Title -}}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p
|
||||||
|
class="text-chalk text-base md:text-lg max-w-2xl mb-10 leading-relaxed text-pretty font-body opacity-90 line-clamp-3"
|
||||||
|
>
|
||||||
|
{{- .Description -}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- CTA + author -->
|
||||||
|
<div class="flex flex-wrap items-center gap-6">
|
||||||
|
<a href="{{ .RelPermalink }}" class="btn btn-outline"
|
||||||
|
>View Editorial {{ partial "icon.html" "arrow-right" }}</a
|
||||||
|
>
|
||||||
|
{{- with .Params.author -}}
|
||||||
|
{{- $author := site.GetPage (printf "authors/%s" .) -}}
|
||||||
|
{{- with $author -}}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{{- $avRes := .Resources.GetMatch "avatar.*" -}}
|
||||||
|
{{- $avSrc := "" -}}
|
||||||
|
{{- with $avRes }}
|
||||||
|
{{ $avSrc = .RelPermalink }}
|
||||||
|
{{ else }}
|
||||||
|
{{ with $author.Params.avatar }}{{ $avSrc = . }}{{ end }}
|
||||||
|
{{ end -}}
|
||||||
|
{{- with $avSrc -}}
|
||||||
|
<img
|
||||||
|
src="{{ . }}"
|
||||||
|
alt="{{ $author.Params.name | default $author.Title }}"
|
||||||
|
class="w-8 h-8 object-cover border border-heat/50"
|
||||||
|
/>
|
||||||
|
{{- end -}}
|
||||||
|
<span class="label text-chalk">
|
||||||
|
By
|
||||||
|
<a href="{{ .RelPermalink }}" class="text-heat hover:text-frost transition-colors"
|
||||||
|
>{{ .Params.name | default .Title }}</a
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<!-- Scroll indicator -->
|
||||||
</div>
|
<div class="absolute bottom-8 right-8 z-10 hidden md:flex flex-col items-center gap-2">
|
||||||
|
<span class="label text-fog" style="writing-mode: vertical-lr; letter-spacing: 0.25em;">SCROLL</span>
|
||||||
<!-- Scroll indicator -->
|
<div class="w-px h-12 bg-zinc relative overflow-hidden">
|
||||||
<div class="absolute bottom-8 right-8 z-10 hidden md:flex flex-col items-center gap-2">
|
<div
|
||||||
<span class="label text-fog" style="writing-mode: vertical-lr; letter-spacing: 0.25em;">SCROLL</span>
|
class="absolute top-0 left-0 right-0 h-full"
|
||||||
<div class="w-px h-12 bg-zinc relative overflow-hidden">
|
style="background: linear-gradient(var(--color-heat), var(--color-frost));
|
||||||
<div class="absolute top-0 left-0 right-0 h-full"
|
animation: scroll-line 1.8s ease-in-out infinite;"
|
||||||
style="background: linear-gradient(var(--color-heat), var(--color-frost));
|
></div>
|
||||||
animation: scroll-line 1.8s ease-in-out infinite;"></div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
</section>
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
<div class="gradient-line" style="opacity:0.2"></div>
|
|
||||||
|
|
||||||
<!-- ── LATEST POSTS ──────────────────────────────────────────── -->
|
|
||||||
<section class="gutter-x py-16 md:py-24 relative overflow-hidden">
|
|
||||||
|
|
||||||
<!-- Decorative graffiti tag behind heading -->
|
|
||||||
<div class="graffiti-tag absolute -top-6 -right-4 opacity-100 select-none pointer-events-none"
|
|
||||||
style="-webkit-text-stroke-color: rgba(0,200,255,0.07);" aria-hidden="true">DROPS</div>
|
|
||||||
|
|
||||||
<div class="relative z-10 flex flex-wrap items-end justify-between gap-4 mb-10">
|
|
||||||
<h2 class="font-display text-5xl md:text-7xl leading-none">
|
|
||||||
<span class="text-stroke-heat">LATEST</span> <span class="text-gradient">DROPS</span>
|
|
||||||
</h2>
|
|
||||||
<a href="/posts/" class="btn btn-ghost">All posts {{ partial "icon.html" "arrow-right" }}</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative z-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
|
||||||
{{- range first 8 $latest -}}
|
|
||||||
{{- partial "post-card.html" . -}}
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{- if gt (len $latest) 8 -}}
|
|
||||||
<div class="relative z-10 mt-14 text-center">
|
|
||||||
<a href="/posts/" class="btn btn-outline">View All {{ len $posts }} Posts {{ partial "icon.html" "arrow-right" }}</a>
|
|
||||||
</div>
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- ── ABOUT STRIP ───────────────────────────────────────────── -->
|
<div class="gradient-line" style="opacity:0.2"></div>
|
||||||
<section class="bg-concrete border-t border-b border-zinc gutter-x py-16 md:py-20 spray-bg relative overflow-hidden">
|
|
||||||
<div class="max-w-3xl mx-auto text-center relative z-10">
|
|
||||||
<span class="badge badge-frost text-void mb-6 inline-block">About the Magazine</span>
|
|
||||||
<p class="font-display text-3xl md:text-5xl text-paper leading-tight text-balance mb-10">
|
|
||||||
{{ .Site.Params.description }}
|
|
||||||
</p>
|
|
||||||
<a href="/about/" class="btn btn-outline">About the magazine {{ partial "icon.html" "arrow-right" }}</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
|
<!-- ── LATEST POSTS ──────────────────────────────────────────── -->
|
||||||
|
<section class="gutter-x py-16 md:py-24 relative overflow-hidden">
|
||||||
|
<!-- Decorative graffiti tag behind heading -->
|
||||||
|
<div
|
||||||
|
class="graffiti-tag absolute -top-6 -right-4 opacity-100 select-none pointer-events-none"
|
||||||
|
style="-webkit-text-stroke-color: rgba(0,200,255,0.07);"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
DROPS
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex flex-wrap items-end justify-between gap-4 mb-10">
|
||||||
|
<h2 class="font-display text-5xl md:text-7xl leading-none">
|
||||||
|
<span class="text-stroke-heat">LATEST</span> <span class="text-gradient">DROPS</span>
|
||||||
|
</h2>
|
||||||
|
<a href="/posts/" class="btn btn-ghost">All posts {{ partial "icon.html" "arrow-right" }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative z-10 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-5">
|
||||||
|
{{- range first 4 $latest -}}
|
||||||
|
{{- partial "post-card.html" . -}}
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{- if gt (len $latest) 4 -}}
|
||||||
|
<div class="relative z-10 mt-14 text-center">
|
||||||
|
<a href="/posts/" class="btn btn-outline"
|
||||||
|
>View All {{ len $posts }} Posts {{ partial "icon.html" "arrow-right" }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── ABOUT STRIP ───────────────────────────────────────────── -->
|
||||||
|
<section class="bg-concrete border-t border-b border-zinc gutter-x py-16 md:py-20 spray-bg relative overflow-hidden">
|
||||||
|
<div class="max-w-3xl mx-auto text-center relative z-10">
|
||||||
|
<span class="badge badge-frost text-void mb-6 inline-block">About the Magazine</span>
|
||||||
|
<p class="font-display text-3xl md:text-5xl text-paper leading-tight text-balance mb-10">
|
||||||
|
{{ .Site.Params.description }}
|
||||||
|
</p>
|
||||||
|
<a href="/about/" class="btn btn-outline">About the magazine {{ partial "icon.html" "arrow-right" }}</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,24 +1,39 @@
|
|||||||
{{- /* Inline author byline for post single pages.
|
{{- /* Inline author byline for post single pages.
|
||||||
Expects the author page as context (.).
|
Expects the author page as context (.).
|
||||||
*/ -}}
|
*/
|
||||||
|
-}}
|
||||||
<div class="flex items-center gap-4 py-6 border-t border-b border-zinc my-8 max-w-2xl">
|
<div class="flex items-center gap-4 py-6 border-t border-b border-zinc my-8 max-w-2xl">
|
||||||
{{- $avatarRes := .Resources.GetMatch "avatar.*" -}}
|
{{- $avatarRes := .Resources.GetMatch "avatar.*" -}}
|
||||||
{{- if $avatarRes -}}
|
{{- if $avatarRes -}}
|
||||||
{{- $av := $avatarRes.Resize "96x webp" -}}
|
{{- $av := $avatarRes.Resize "96x webp" -}}
|
||||||
<a href="{{ $.RelPermalink }}" class="flex-shrink-0">
|
<a href="{{ $.RelPermalink }}" class="flex-shrink-0">
|
||||||
<img src="{{ $av.RelPermalink }}" alt="{{ $.Params.name | default $.Title }}" class="w-16 h-16 object-cover border border-zinc hover:border-heat transition-colors" loading="lazy" decoding="async">
|
<img
|
||||||
|
src="{{ $av.RelPermalink }}"
|
||||||
|
alt="{{ $.Params.name | default $.Title }}"
|
||||||
|
class="w-16 h-16 object-cover border border-zinc hover:border-heat transition-colors"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
{{- else -}}{{- with .Params.avatar -}}
|
{{- else -}}
|
||||||
<a href="{{ $.RelPermalink }}" class="flex-shrink-0">
|
{{- with .Params.avatar -}}
|
||||||
<img src="{{ . }}" alt="{{ $.Params.name | default $.Title }}" class="w-16 h-16 object-cover border border-zinc hover:border-heat transition-colors" loading="lazy" decoding="async">
|
<a href="{{ $.RelPermalink }}" class="flex-shrink-0">
|
||||||
</a>
|
<img
|
||||||
{{- end -}}{{- end -}}
|
src="{{ . }}"
|
||||||
|
alt="{{ $.Params.name | default $.Title }}"
|
||||||
|
class="w-16 h-16 object-cover border border-zinc hover:border-heat transition-colors"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<a href="{{ .RelPermalink }}" class="text-base font-display text-heat hover:text-frost transition-colors block">
|
<a href="{{ .RelPermalink }}" class="text-base font-display text-heat hover:text-frost transition-colors block">
|
||||||
{{ .Params.name | default .Title }}
|
{{ .Params.name | default .Title }}
|
||||||
</a>
|
</a>
|
||||||
{{- with .Params.bio -}}
|
{{- with .Params.bio -}}
|
||||||
<p class="text-fog text-sm leading-relaxed line-clamp-2 font-body">{{ . }}</p>
|
<p class="text-fog text-md leading-relaxed line-clamp-2 font-body">{{ . }}</p>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<footer class="relative overflow-hidden border-t border-zinc gutter-x py-14 mt-auto bg-ink speed-lines">
|
<footer class="relative overflow-hidden border-t border-zinc gutter-x py-14 mt-auto bg-ink speed-lines">
|
||||||
|
|
||||||
<!-- Decorative background text -->
|
<!-- Decorative background text -->
|
||||||
<div
|
<div
|
||||||
class="graffiti-tag absolute -bottom-6 -right-4 select-none pointer-events-none"
|
class="graffiti-tag absolute -bottom-6 -right-4 select-none pointer-events-none"
|
||||||
style="-webkit-text-stroke-color: rgba(255,26,140,0.07); opacity: 1;"
|
style="-webkit-text-stroke-color: rgba(255,26,140,0.07); opacity: 1;"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>PIVOINE</div>
|
>
|
||||||
|
PIVOINE
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="gradient-line mb-10 relative z-10"></div>
|
<div class="gradient-line mb-10 relative z-10"></div>
|
||||||
<div class="relative z-10 flex flex-col md:flex-row md:items-start justify-between gap-10">
|
<div class="relative z-10 flex flex-col md:flex-row md:items-start justify-between gap-10">
|
||||||
|
|
||||||
<!-- Brand -->
|
<!-- Brand -->
|
||||||
<div>
|
<div>
|
||||||
<a href="/" class="block mb-4 hover:opacity-90 transition-opacity w-fit" aria-label="{{ .Site.Title }} Home">
|
<a href="/" class="block mb-4 hover:opacity-90 transition-opacity w-fit" aria-label="{{ .Site.Title }} Home">
|
||||||
@@ -22,17 +22,34 @@
|
|||||||
<nav aria-label="Footer navigation">
|
<nav aria-label="Footer navigation">
|
||||||
<ul class="flex flex-wrap gap-x-8 gap-y-4">
|
<ul class="flex flex-wrap gap-x-8 gap-y-4">
|
||||||
{{- range .Site.Menus.footer -}}
|
{{- range .Site.Menus.footer -}}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ .URL }}" class="label text-fog hover:text-heat transition-colors">{{ .Name }}</a>
|
{{- if hasPrefix .URL "http" -}}
|
||||||
</li>
|
<a
|
||||||
|
href="{{ .URL }}"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="label text-fog hover:text-heat transition-colors"
|
||||||
|
>
|
||||||
|
{{- .Name -}}{{ partial "icon.html" "arrow-external" }}
|
||||||
|
</a>
|
||||||
|
{{- else -}}
|
||||||
|
<a href="{{ .URL }}" class="label text-fog hover:text-heat transition-colors">{{ .Name }}</a>
|
||||||
|
{{- end -}}
|
||||||
|
</li>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- with .Site.Params.social -}}
|
{{- with .Site.Params.social -}}
|
||||||
{{- range $platform, $url := . -}}
|
{{- range $platform, $url := . -}}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ $url }}" target="_blank" rel="noopener noreferrer" class="label text-fog hover:text-frost transition-colors">
|
<a
|
||||||
{{- $platform | upper -}} {{ partial "icon.html" "arrow-external" }}
|
href="{{ $url }}"
|
||||||
</a>
|
target="_blank"
|
||||||
</li>
|
rel="noopener noreferrer"
|
||||||
|
class="label text-fog hover:text-frost transition-colors"
|
||||||
|
>
|
||||||
|
{{- $platform | upper -}}
|
||||||
|
{{ partial "icon.html" "arrow-external" }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,81 +1,133 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="htmx-config" content='{"globalViewTransitions":true,"scrollBehavior":"smooth"}'>
|
<meta name="htmx-config" content='{"globalViewTransitions":true,"scrollBehavior":"smooth"}' />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#FF1A8C">
|
<meta name="theme-color" content="#FF1A8C" />
|
||||||
|
|
||||||
<title>
|
<title>
|
||||||
{{- if .IsHome -}}
|
{{- if .IsHome -}}
|
||||||
{{- .Site.Title }} — {{ .Site.Params.tagline -}}
|
{{- .Site.Title }} —
|
||||||
|
{{ .Site.Params.tagline -}}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
{{- .Title }} — {{ .Site.Title -}}
|
{{- .Title }} —
|
||||||
|
{{ .Site.Title -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</title>
|
</title>
|
||||||
|
|
||||||
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
|
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}" />
|
||||||
<meta name="author" content="{{ with .Params.author }}{{ . }}{{ else }}{{ .Site.Params.author }}{{ end }}">
|
<meta name="author" content="{{ with .Params.author }}{{ . }}{{ else }}{{ .Site.Params.author }}{{ end }}" />
|
||||||
{{- if .Site.Params.robots }}<meta name="robots" content="{{ .Site.Params.robots }}">{{ end }}
|
{{- if .Site.Params.robots }}<meta name="robots" content="{{ .Site.Params.robots }}" />{{ end }}
|
||||||
|
|
||||||
|
|
||||||
<!-- Open Graph -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:site_name" content="{{ .Site.Title }}">
|
<meta property="og:site_name" content="{{ .Site.Title }}" />
|
||||||
<meta property="og:title" content="{{ if .IsHome }}{{ .Site.Title }} — {{ .Site.Params.tagline }}{{ else }}{{ .Title }}{{ end }}">
|
<meta
|
||||||
<meta property="og:description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
|
property="og:title"
|
||||||
<meta property="og:url" content="{{ .Permalink }}">
|
content="{{ if .IsHome }}
|
||||||
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}">
|
{{ .Site.Title }} —
|
||||||
<meta property="og:locale" content="{{ .Site.LanguageCode | default "en_US" | replaceRE "-" "_" }}">
|
{{ .Site.Params.tagline }}
|
||||||
|
{{ else }}
|
||||||
|
{{ .Title }}
|
||||||
|
{{ end }}"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="{{ with .Description }}
|
||||||
|
{{ . }}
|
||||||
|
{{ else }}
|
||||||
|
{{ .Site.Params.description }}
|
||||||
|
{{ end }}"
|
||||||
|
/>
|
||||||
|
<meta property="og:url" content="{{ .Permalink }}" />
|
||||||
|
<meta property="og:type" content="{{ if .IsPage }}article{{ else }}website{{ end }}" />
|
||||||
|
<meta property="og:locale" content="{{ .Site.Language.Locale | default "en_US" | replaceRE "-" "_" }}" />
|
||||||
{{- $ogImage := .Site.Params.ogImage -}}
|
{{- $ogImage := .Site.Params.ogImage -}}
|
||||||
{{- $ogImageAlt := .Site.Title -}}
|
{{- $ogImageAlt := .Site.Title -}}
|
||||||
{{- with .Params.banner -}}
|
{{- with .Params.banner -}}
|
||||||
{{- $ogImage = .src -}}
|
{{- $ogImage = .src -}}
|
||||||
{{- $ogImageAlt = $.Title -}}
|
{{- $ogImageAlt = $.Title -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<meta property="og:image" content="{{ $ogImage | absURL }}">
|
<meta property="og:image" content="{{ $ogImage | absURL }}" />
|
||||||
<meta property="og:image:alt" content="{{ $ogImageAlt }}">
|
<meta property="og:image:alt" content="{{ $ogImageAlt }}" />
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200" />
|
||||||
<meta property="og:image:height" content="630">
|
<meta property="og:image:height" content="630" />
|
||||||
|
|
||||||
{{- if .IsPage }}
|
{{- if .IsPage }}
|
||||||
<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
|
<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}" />
|
||||||
<meta property="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}">
|
<meta property="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}" />
|
||||||
{{- with .Params.author }}<meta property="article:author" content="{{ . }}">{{ end }}
|
{{- with .Params.author }}<meta property="article:author" content="{{ . }}" />{{ end }}
|
||||||
{{- range .Params.categories }}<meta property="article:section" content="{{ . }}">{{ end }}
|
{{- range .Params.categories }}<meta property="article:section" content="{{ . }}" />{{ end }}
|
||||||
{{- range .Params.tags }}<meta property="article:tag" content="{{ . }}">{{ end }}
|
{{- range .Params.tags }}<meta property="article:tag" content="{{ . }}" />{{ end }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
|
||||||
<!-- Twitter / X Card -->
|
<!-- Twitter / X Card -->
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
{{- with .Site.Params.twitterHandle }}
|
{{- with .Site.Params.twitterHandle }}
|
||||||
<meta name="twitter:site" content="@{{ . }}">
|
<meta name="twitter:site" content="@{{ . }}" />
|
||||||
{{- end }}
|
{{- end }}
|
||||||
<meta name="twitter:title" content="{{ if .IsHome }}{{ .Site.Title }} — {{ .Site.Params.tagline }}{{ else }}{{ .Title }}{{ end }}">
|
<meta
|
||||||
<meta name="twitter:description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
|
name="twitter:title"
|
||||||
<meta name="twitter:image" content="{{ $ogImage | absURL }}">
|
content="{{ if .IsHome }}
|
||||||
<meta name="twitter:image:alt" content="{{ $ogImageAlt }}">
|
{{ .Site.Title }} —
|
||||||
|
{{ .Site.Params.tagline }}
|
||||||
|
{{ else }}
|
||||||
|
{{ .Title }}
|
||||||
|
{{ end }}"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="{{ with .Description }}
|
||||||
|
{{ . }}
|
||||||
|
{{ else }}
|
||||||
|
{{ .Site.Params.description }}
|
||||||
|
{{ end }}"
|
||||||
|
/>
|
||||||
|
<meta name="twitter:image" content="{{ $ogImage | absURL }}" />
|
||||||
|
<meta name="twitter:image:alt" content="{{ $ogImageAlt }}" />
|
||||||
|
|
||||||
<!-- Canonical -->
|
<!-- Canonical -->
|
||||||
<link rel="canonical" href="{{ .Permalink }}">
|
<link rel="canonical" href="{{ .Permalink }}" />
|
||||||
|
|
||||||
<!-- JSON-LD structured data -->
|
<!-- JSON-LD structured data -->
|
||||||
{{- partial "schema.html" . -}}
|
{{- partial "schema.html" . -}}
|
||||||
|
|
||||||
<!-- Fonts — Bebas Neue + Barlow + Share Tech Mono (non-render-blocking) -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Share+Tech+Mono&display=swap">
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Share+Tech+Mono&display=swap" media="print" onload="this.media='all'">
|
|
||||||
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Share+Tech+Mono&display=swap"></noscript>
|
|
||||||
|
|
||||||
<!-- CSS via Hugo Pipes + PostCSS + Tailwind v4 -->
|
<!-- Fonts — Bebas Neue + Barlow + Share Tech Mono (non-render-blocking) -->
|
||||||
{{- $css := resources.Get "css/main.css" | css.PostCSS -}}
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
rel="preload"
|
||||||
|
as="style"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Share+Tech+Mono&display=swap"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Share+Tech+Mono&display=swap"
|
||||||
|
media="print"
|
||||||
|
onload="this.media='all'"
|
||||||
|
/>
|
||||||
|
<noscript
|
||||||
|
><link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow:ital,wght@0,300;0,400;0,500;0,600;1,400&family=Share+Tech+Mono&display=swap"
|
||||||
|
/></noscript>
|
||||||
|
|
||||||
|
<!-- CSS via Hugo Pipes + Tailwind v4 (native css.TailwindCSS, no PostCSS) -->
|
||||||
|
{{- $css := resources.Get "css/main.css" | css.TailwindCSS -}}
|
||||||
{{- if eq hugo.Environment "production" -}}
|
{{- if eq hugo.Environment "production" -}}
|
||||||
{{- $css = $css | minify | fingerprint "sha256" -}}
|
{{- $css = $css | minify | fingerprint "sha256" -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<link rel="stylesheet" href="{{ $css.RelPermalink }}"{{ if eq hugo.Environment "production" }} integrity="{{ $css.Data.Integrity }}"{{ end }}>
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="{{ $css.RelPermalink }}"
|
||||||
|
{{ if eq hugo.Environment "production" }}integrity="{{ $css.Data.Integrity }}"{{ end }}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Favicon & PWA -->
|
<!-- Favicon & PWA -->
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="any">
|
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||||
<link rel="manifest" href="/site.webmanifest">
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<meta name="msapplication-TileColor" content="#050510">
|
<meta name="msapplication-TileColor" content="#050510" />
|
||||||
|
|||||||
@@ -1,11 +1,55 @@
|
|||||||
{{- /* Inline SVG icon. Usage: {{ partial "icon.html" "arrow-right" }}
|
{{- /* Inline SVG icon. Usage: {{ partial "icon.html" "arrow-right" }}
|
||||||
Renders at 1em × 1em, inherits currentColor, aria-hidden.
|
Renders at 1em × 1em, inherits currentColor, aria-hidden. */ -}}
|
||||||
*/ -}}
|
|
||||||
{{- $name := . -}}
|
{{- $name := . -}}
|
||||||
{{- if eq $name "arrow-right" -}}
|
{{- if eq $name "arrow-right" -}}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:inline-block;vertical-align:-0.1em"><path d="M2 8h12"/><path d="M9 3l5 5-5 5"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
style="display:inline-block;vertical-align:-0.1em"
|
||||||
|
>
|
||||||
|
<path d="M2 8h12" />
|
||||||
|
<path d="M9 3l5 5-5 5" />
|
||||||
|
</svg>
|
||||||
{{- else if eq $name "arrow-left" -}}
|
{{- else if eq $name "arrow-left" -}}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:inline-block;vertical-align:-0.1em"><path d="M14 8H2"/><path d="M7 3L2 8l5 5"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
style="display:inline-block;vertical-align:-0.1em"
|
||||||
|
>
|
||||||
|
<path d="M14 8H2" />
|
||||||
|
<path d="M7 3L2 8l5 5" />
|
||||||
|
</svg>
|
||||||
{{- else if eq $name "arrow-external" -}}
|
{{- else if eq $name "arrow-external" -}}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="display:inline-block;vertical-align:-0.1em"><path d="M4 12L12 4"/><path d="M6 4h6v6"/></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
aria-hidden="true"
|
||||||
|
style="display:inline-block;vertical-align:-0.1em"
|
||||||
|
>
|
||||||
|
<path d="M4 12L12 4" />
|
||||||
|
<path d="M6 4h6v6" />
|
||||||
|
</svg>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
Renders a Hugo image resource as an optimised WebP <img> with responsive srcset.
|
Renders a Hugo image resource as an optimised WebP <img> with responsive srcset.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
res — Hugo image resource (required)
|
res — Hugo image resource (required)
|
||||||
widths — slice of pixel widths to generate, e.g. (slice 600 1200)
|
widths — slice of pixel widths to generate, e.g. (slice 600 1200)
|
||||||
default: (slice 800 1600)
|
default: (slice 800 1600)
|
||||||
sizes — value for the <img sizes> attribute
|
sizes — value for the <img sizes> attribute
|
||||||
default: "100vw"
|
default: "100vw"
|
||||||
class — CSS classes applied to <img>
|
class — CSS classes applied to <img>
|
||||||
alt — alt text (default "")
|
alt — alt text (default "")
|
||||||
loading — "lazy" or "eager" (default "lazy")
|
loading — "lazy" or "eager" (default "lazy")
|
||||||
*/ -}}
|
*/
|
||||||
|
-}}
|
||||||
{{- $res := .res -}}
|
{{- $res := .res -}}
|
||||||
{{- $widths := .widths | default (slice 800 1600) -}}
|
{{- $widths := .widths | default (slice 800 1600) -}}
|
||||||
{{- $sizes := .sizes | default "100vw" -}}
|
{{- $sizes := .sizes | default "100vw" -}}
|
||||||
@@ -35,4 +36,4 @@
|
|||||||
height="{{ $primary.Height }}"
|
height="{{ $primary.Height }}"
|
||||||
loading="{{ $loading }}"
|
loading="{{ $loading }}"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
>
|
/>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
{{- /*
|
{{- /*
|
||||||
Logo image.
|
Logo image.
|
||||||
Usage: {{ partial "logo.html" (dict "class" "h-8 w-auto") }}
|
Usage: {{ partial "logo.html" (dict "class" "h-8 w-auto")
|
||||||
Params:
|
}}
|
||||||
class — CSS classes on the <img> element (default: "h-9 w-auto")
|
Params: class — CSS classes on the <img /> element (default: "h-9 w-auto") */ -}}
|
||||||
*/ -}}
|
|
||||||
{{- $class := .class | default "h-9 w-auto" -}}
|
{{- $class := .class | default "h-9 w-auto" -}}
|
||||||
<img src="/logo.svg" class="{{ $class }} logo-img" alt="{{ site.Title }}" width="512" height="512">
|
<img src="/logo.svg" class="{{ $class }} logo-img" alt="{{ site.Title }}" width="512" height="512" />
|
||||||
|
|||||||
@@ -2,35 +2,32 @@
|
|||||||
Renders an image or video from a front matter map.
|
Renders an image or video from a front matter map.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
{{ partial "media.html" (dict "media" .Params.banner "class" "w-full h-full object-cover") }}
|
{{ partial "media.html" (dict "media" .Params.banner "class" "w-full h-full object-cover")
|
||||||
|
}}
|
||||||
Parameters:
|
|
||||||
media — map with keys: type (image|video), src, alt
|
|
||||||
class — CSS classes applied to the img/video element
|
|
||||||
lazy — set to false to disable lazy loading (default true)
|
|
||||||
*/ -}}
|
|
||||||
|
|
||||||
|
Parameters: media — map with keys: type (image|video), src, alt class — CSS classes applied to the img/video element
|
||||||
|
lazy — set to false to disable lazy loading (default true) */ -}}
|
||||||
{{- $media := .media -}}
|
{{- $media := .media -}}
|
||||||
{{- $class := .class | default "" -}}
|
{{- $class := .class | default "" -}}
|
||||||
{{- $lazy := .lazy | default true -}}
|
{{- $lazy := .lazy | default true -}}
|
||||||
|
|
||||||
{{- with $media -}}
|
{{- with $media -}}
|
||||||
{{- if eq .type "video" -}}
|
{{- if eq .type "video" -}}
|
||||||
<video
|
<video
|
||||||
class="{{ $class }}"
|
class="{{ $class }}"
|
||||||
src="{{ .src }}"
|
src="{{ .src }}"
|
||||||
autoplay
|
autoplay
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
{{- with .alt }} aria-label="{{ . }}"{{ end }}
|
{{- with .alt }}aria-label="{{ . }}"{{ end }}
|
||||||
></video>
|
></video>
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
<img
|
<img
|
||||||
class="{{ $class }}"
|
class="{{ $class }}"
|
||||||
src="{{ .src }}"
|
src="{{ .src }}"
|
||||||
alt="{{ .alt | default "" }}"
|
alt="{{ .alt | default "" }}"
|
||||||
{{- if $lazy }} loading="lazy" decoding="async"{{ end }}
|
{{- if $lazy }}loading="lazy" decoding="async"{{ end }}
|
||||||
>
|
/>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
:class="$store.nav.open || scrolled ? 'bg-ink/95 backdrop-blur-md shadow-[0_1px_0_var(--color-zinc)]' : ''"
|
:class="$store.nav.open || scrolled ? 'bg-ink/95 backdrop-blur-md shadow-[0_1px_0_var(--color-zinc)]' : ''"
|
||||||
>
|
>
|
||||||
<!-- Logotype -->
|
<!-- Logotype -->
|
||||||
<a href="/" class="flex items-center gap-3 hover:opacity-90 transition-opacity" aria-label="{{ .Site.Params.logoText }} Home">
|
<a
|
||||||
|
href="/"
|
||||||
|
class="flex items-center gap-3 hover:opacity-90 transition-opacity"
|
||||||
|
aria-label="{{ .Site.Params.logoText }} Home"
|
||||||
|
>
|
||||||
{{- partial "logo.html" (dict "id" "nav" "class" "h-12 w-auto flex-shrink-0") -}}
|
{{- partial "logo.html" (dict "id" "nav" "class" "h-12 w-auto flex-shrink-0") -}}
|
||||||
<span class="logo-glitch font-display text-2xl md:text-3xl leading-none tracking-wide">
|
<span class="logo-glitch font-display text-2xl md:text-3xl leading-none tracking-wide">
|
||||||
{{- .Site.Params.logoText -}}
|
{{- .Site.Params.logoText -}}
|
||||||
@@ -20,19 +24,19 @@
|
|||||||
<!-- Desktop navigation -->
|
<!-- Desktop navigation -->
|
||||||
<nav class="hidden md:flex items-center gap-8" aria-label="Primary navigation">
|
<nav class="hidden md:flex items-center gap-8" aria-label="Primary navigation">
|
||||||
{{- range .Site.Menus.main -}}
|
{{- range .Site.Menus.main -}}
|
||||||
{{- $href := .URL -}}
|
{{- $href := .URL -}}
|
||||||
<a
|
<a
|
||||||
href="{{ $href }}"
|
href="{{ $href }}"
|
||||||
class="label relative transition-colors"
|
class="label relative transition-colors"
|
||||||
:class="($store.nav.path === '{{ $href }}' || ('{{ $href }}' !== '/' && $store.nav.path.startsWith('{{ $href }}'))) ? 'text-heat' : 'text-fog hover:text-chalk'"
|
:class="($store.nav.path === '{{ $href }}' || ('{{ $href }}' !== '/' && $store.nav.path.startsWith('{{ $href }}'))) ? 'text-heat' : 'text-fog hover:text-chalk'"
|
||||||
>
|
>
|
||||||
{{ .Name }}
|
{{ .Name }}
|
||||||
<span
|
<span
|
||||||
class="absolute -bottom-0.5 left-0 h-px bg-heat transition-all duration-300 ease-out"
|
class="absolute -bottom-0.5 left-0 h-px bg-heat transition-all duration-300 ease-out"
|
||||||
:style="($store.nav.path === '{{ $href }}' || ('{{ $href }}' !== '/' && $store.nav.path.startsWith('{{ $href }}'))) ? 'width: 100%' : 'width: 0'"
|
:style="($store.nav.path === '{{ $href }}' || ('{{ $href }}' !== '/' && $store.nav.path.startsWith('{{ $href }}'))) ? 'width: 100%' : 'width: 0'"
|
||||||
style="width: 0"
|
style="width: 0"
|
||||||
></span>
|
></span>
|
||||||
</a>
|
</a>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -45,12 +49,18 @@
|
|||||||
aria-controls="mobile-menu"
|
aria-controls="mobile-menu"
|
||||||
aria-label="Toggle navigation"
|
aria-label="Toggle navigation"
|
||||||
>
|
>
|
||||||
<span class="block h-px w-6 bg-current transition-all duration-300 origin-center"
|
<span
|
||||||
:class="$store.nav.open ? 'rotate-45 translate-y-[7px]' : ''"></span>
|
class="block h-px w-6 bg-current transition-all duration-300 origin-center"
|
||||||
<span class="block h-px w-4 bg-current transition-all duration-200"
|
:class="$store.nav.open ? 'rotate-45 translate-y-[7px]' : ''"
|
||||||
:class="$store.nav.open ? 'opacity-0' : 'opacity-100'"></span>
|
></span>
|
||||||
<span class="block h-px w-6 bg-current transition-all duration-300 origin-center"
|
<span
|
||||||
:class="$store.nav.open ? '-rotate-45 -translate-y-[7px]' : ''"></span>
|
class="block h-px w-4 bg-current transition-all duration-200"
|
||||||
|
:class="$store.nav.open ? 'opacity-0' : 'opacity-100'"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="block h-px w-6 bg-current transition-all duration-300 origin-center"
|
||||||
|
:class="$store.nav.open ? '-rotate-45 -translate-y-[7px]' : ''"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,51 +83,70 @@
|
|||||||
<div class="gradient-line flex-none"></div>
|
<div class="gradient-line flex-none"></div>
|
||||||
|
|
||||||
<!-- Decorative background text -->
|
<!-- Decorative background text -->
|
||||||
<div class="graffiti-tag absolute -bottom-4 -right-4 select-none pointer-events-none"
|
<div
|
||||||
style="-webkit-text-stroke-color: rgba(155,0,255,0.07);" aria-hidden="true">MENU</div>
|
class="graffiti-tag absolute -bottom-4 -right-4 select-none pointer-events-none"
|
||||||
|
style="-webkit-text-stroke-color: rgba(155,0,255,0.07);"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
MENU
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main nav links -->
|
<!-- Main nav links -->
|
||||||
<nav class="flex-1 flex flex-col justify-center gutter-x py-10" aria-label="Mobile navigation">
|
<nav class="flex-1 flex flex-col justify-center gutter-x py-10" aria-label="Mobile navigation">
|
||||||
<div class="flex flex-col gap-0">
|
<div class="flex flex-col gap-0">
|
||||||
{{- $i := 0 -}}
|
{{- $i := 0 -}}
|
||||||
{{- range .Site.Menus.main -}}
|
{{- range .Site.Menus.main -}}
|
||||||
{{- $href := .URL -}}
|
{{- $href := .URL -}}
|
||||||
<a
|
<a
|
||||||
href="{{ $href }}"
|
href="{{ $href }}"
|
||||||
class="group relative leading-none py-3 overflow-hidden"
|
class="group relative leading-none py-3 overflow-hidden"
|
||||||
style="font-family: var(--font-display); font-size: clamp(2.8rem, 11vw, 6.5rem); animation: mob-link-in 0.5s var(--ease-out-expo) {{ mul $i 70 }}ms both"
|
style="font-family: var(--font-display); font-size: clamp(2.8rem, 11vw, 6.5rem); animation: mob-link-in 0.5s var(--ease-out-expo) {{ mul $i 70 }}ms both"
|
||||||
@click="$store.nav.open = false"
|
@click="$store.nav.open = false"
|
||||||
>
|
>
|
||||||
<!-- Hover sweep -->
|
<!-- Hover sweep -->
|
||||||
<span class="absolute inset-y-0 left-0 w-0 group-hover:w-full transition-all duration-300 ease-out"
|
<span
|
||||||
style="background: linear-gradient(90deg, rgba(255,26,140,0.07), transparent);"></span>
|
class="absolute inset-y-0 left-0 w-0 group-hover:w-full transition-all duration-300 ease-out"
|
||||||
<!-- Link text -->
|
style="background: linear-gradient(90deg, rgba(255,26,140,0.07), transparent);"
|
||||||
<span class="relative transition-colors duration-200 text-paper group-hover:text-heat uppercase"
|
></span>
|
||||||
:class="($store.nav.path === '{{ $href }}' || ('{{ $href }}' !== '/' && $store.nav.path.startsWith('{{ $href }}'))) ? 'text-gradient' : ''"
|
<!-- Link text -->
|
||||||
>{{ .Name }}</span>
|
<span
|
||||||
<!-- Index number -->
|
class="relative transition-colors duration-200 text-paper group-hover:text-heat uppercase"
|
||||||
<span class="absolute right-0 top-1/2 -translate-y-1/2 label text-smoke group-hover:text-heat transition-colors duration-200"
|
:class="($store.nav.path === '{{ $href }}' || ('{{ $href }}' !== '/' && $store.nav.path.startsWith('{{ $href }}'))) ? 'text-gradient' : ''"
|
||||||
style="animation: mob-link-in 0.4s var(--ease-out-expo) {{ add (mul $i 70) 100 }}ms both"
|
>{{ .Name }}</span
|
||||||
>0{{ add $i 1 }}</span>
|
>
|
||||||
</a>
|
<!-- Index number -->
|
||||||
{{- $i = add $i 1 -}}
|
<span
|
||||||
|
class="absolute right-0 top-1/2 -translate-y-1/2 label text-smoke group-hover:text-heat transition-colors duration-200"
|
||||||
|
style="animation: mob-link-in 0.4s var(--ease-out-expo) {{ add (mul $i 70) 100 }}ms both"
|
||||||
|
>0{{ add $i 1 }}</span
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
{{- $i = add $i 1 -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gradient-line my-8"
|
<div class="gradient-line my-8" style="animation: mob-link-in 0.4s ease {{ mul $i 70 }}ms both"></div>
|
||||||
style="animation: mob-link-in 0.4s ease {{ mul $i 70 }}ms both"></div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-x-8 gap-y-3"
|
<div
|
||||||
style="animation: mob-link-in 0.4s ease {{ add (mul $i 70) 40 }}ms both">
|
class="flex flex-wrap gap-x-8 gap-y-3"
|
||||||
|
style="animation: mob-link-in 0.4s ease {{ add (mul $i 70) 40 }}ms both"
|
||||||
|
>
|
||||||
{{- range .Site.Menus.footer -}}
|
{{- range .Site.Menus.footer -}}
|
||||||
<a href="{{ .URL }}" class="label text-fog hover:text-heat transition-colors" @click="$store.nav.open = false">{{ .Name }}</a>
|
<a
|
||||||
|
href="{{ .URL }}"
|
||||||
|
class="label text-fog hover:text-heat transition-colors"
|
||||||
|
@click="$store.nav.open = false"
|
||||||
|
>{{ .Name }}</a
|
||||||
|
>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Bottom bar -->
|
<!-- Bottom bar -->
|
||||||
<div class="flex-none gutter-x pb-8 pt-4 border-t border-zinc"
|
<div
|
||||||
style="animation: mob-link-in 0.4s ease 420ms both">
|
class="flex-none gutter-x pb-8 pt-4 border-t border-zinc"
|
||||||
|
style="animation: mob-link-in 0.4s ease 420ms both"
|
||||||
|
>
|
||||||
<p class="label text-smoke">{{ .Site.Params.tagline }}</p>
|
<p class="label text-smoke">{{ .Site.Params.tagline }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -125,8 +154,14 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@keyframes mob-link-in {
|
@keyframes mob-link-in {
|
||||||
from { opacity: 0; transform: translateY(14px); }
|
from {
|
||||||
to { opacity: 1; transform: translateY(0); }
|
opacity: 0;
|
||||||
}
|
transform: translateY(14px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,33 +1,40 @@
|
|||||||
{{- /* Custom pagination component.
|
{{- /* Custom pagination component.
|
||||||
Call as: {{ partial "pagination.html" . }}
|
Call as: {{ partial "pagination.html" .
|
||||||
|
}}
|
||||||
*/ -}}
|
*/ -}}
|
||||||
{{- $paginator := .Paginator -}}
|
{{- $paginator := .Paginator -}}
|
||||||
{{- if gt $paginator.TotalPages 1 -}}
|
{{- if gt $paginator.TotalPages 1 -}}
|
||||||
<nav class="flex items-center justify-center gap-2 py-16" aria-label="Pagination">
|
<nav class="flex items-center justify-center gap-2 py-16" aria-label="Pagination">
|
||||||
|
{{- if $paginator.HasPrev -}}
|
||||||
|
<a
|
||||||
|
href="{{ $paginator.Prev.URL }}"
|
||||||
|
class="label border border-mist text-ash px-5 py-3 hover:border-gold hover:text-gold transition-all duration-300"
|
||||||
|
aria-label="Previous page"
|
||||||
|
>{{ partial "icon.html" "arrow-left" }} Prev</a
|
||||||
|
>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{- if $paginator.HasPrev -}}
|
{{- range $paginator.Pagers -}}
|
||||||
<a
|
<a
|
||||||
href="{{ $paginator.Prev.URL }}"
|
href="{{ .URL }}"
|
||||||
class="label border border-mist text-ash px-5 py-3 hover:border-gold hover:text-gold transition-all duration-300"
|
class="label border px-4 py-3 transition-all duration-300 {{ if eq . $paginator }}
|
||||||
aria-label="Previous page"
|
border-gold text-gold
|
||||||
>{{ partial "icon.html" "arrow-left" }} Prev</a>
|
{{ else }}
|
||||||
{{- end -}}
|
border-mist text-ash hover:border-gold hover:text-gold
|
||||||
|
{{ end }}"
|
||||||
|
{{- if eq . $paginator }}aria-current="page"{{ end }}
|
||||||
|
>{{ .PageNumber }}</a
|
||||||
|
>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{- range $paginator.Pagers -}}
|
{{- if $paginator.HasNext -}}
|
||||||
<a
|
<a
|
||||||
href="{{ .URL }}"
|
href="{{ $paginator.Next.URL }}"
|
||||||
class="label border px-4 py-3 transition-all duration-300 {{ if eq . $paginator }}border-gold text-gold{{ else }}border-mist text-ash hover:border-gold hover:text-gold{{ end }}"
|
class="label border border-mist text-ash px-5 py-3 hover:border-gold hover:text-gold transition-all duration-300"
|
||||||
{{- if eq . $paginator }} aria-current="page"{{ end }}
|
aria-label="Next page"
|
||||||
>{{ .PageNumber }}</a>
|
>Next {{ partial "icon.html" "arrow-right" }}</a
|
||||||
{{- end -}}
|
>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
{{- if $paginator.HasNext -}}
|
</nav>
|
||||||
<a
|
|
||||||
href="{{ $paginator.Next.URL }}"
|
|
||||||
class="label border border-mist text-ash px-5 py-3 hover:border-gold hover:text-gold transition-all duration-300"
|
|
||||||
aria-label="Next page"
|
|
||||||
>Next {{ partial "icon.html" "arrow-right" }}</a>
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
</nav>
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,50 +1,56 @@
|
|||||||
{{- /* Large overlay card for featured/secondary slots.
|
{{- /* Large overlay card for featured/secondary slots.
|
||||||
Expects a post page as context (.).
|
Expects a post page as context (.).
|
||||||
*/ -}}
|
*/
|
||||||
|
-}}
|
||||||
{{- $post := . -}}
|
{{- $post := . -}}
|
||||||
<article class="group relative overflow-hidden aspect-[3/4] md:aspect-[4/5] card-comic">
|
<article class="group relative overflow-hidden aspect-[3/4] md:aspect-[4/5] card-comic">
|
||||||
<a href="{{ $post.RelPermalink }}" class="block w-full h-full" aria-label="{{ $post.Title }}">
|
<a href="{{ $post.RelPermalink }}" class="block w-full h-full" aria-label="{{ $post.Title }}">
|
||||||
|
|
||||||
<!-- Background media — bundle banner.png → 01.png → .Params.banner -->
|
<!-- Background media — bundle banner.png → 01.png → .Params.banner -->
|
||||||
{{- $thumb := $post.Resources.GetMatch "banner.png" -}}
|
{{- $thumb := $post.Resources.GetMatch "banner.png" -}}
|
||||||
{{- if not $thumb -}}{{- $thumb = $post.Resources.GetMatch "01.png" -}}{{- end -}}
|
{{- if not $thumb -}}{{- $thumb = $post.Resources.GetMatch "01.png" -}}{{- end -}}
|
||||||
<div class="card-media w-full h-full absolute inset-0">
|
<div class="card-media w-full h-full absolute inset-0">
|
||||||
{{- if $thumb -}}
|
{{- if $thumb -}}
|
||||||
{{- partial "img.html" (dict "res" $thumb "widths" (slice 800 1200) "sizes" "(max-width: 768px) 100vw, 50vw" "class" "w-full h-full object-cover" "alt" $post.Title) -}}
|
{{- partial "img.html" (dict "res" $thumb "widths" (slice 800 1200) "sizes" "(max-width: 768px) 100vw, 50vw" "class" "w-full h-full object-cover" "alt" $post.Title) -}}
|
||||||
{{- else -}}{{- with $post.Params.banner -}}
|
|
||||||
{{- partial "media.html" (dict "media" . "class" "w-full h-full object-cover") -}}
|
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
<div class="w-full h-full bg-concrete flex items-center justify-center halftone">
|
{{- with $post.Params.banner -}}
|
||||||
<span class="label text-smoke">PIVOINE</span>
|
{{- partial "media.html" (dict "media" . "class" "w-full h-full object-cover") -}}
|
||||||
</div>
|
{{- else -}}
|
||||||
{{- end -}}{{- end -}}
|
<div class="w-full h-full bg-concrete flex items-center justify-center halftone">
|
||||||
|
<span class="label text-smoke">PIVOINE</span>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gradient overlay -->
|
<!-- Gradient overlay -->
|
||||||
<div class="absolute inset-0 overlay-gradient transition-opacity duration-500 group-hover:opacity-90"></div>
|
<div class="absolute inset-0 overlay-gradient transition-opacity duration-500 group-hover:opacity-90"></div>
|
||||||
|
|
||||||
<!-- Diagonal heat accent -->
|
<!-- Diagonal heat accent -->
|
||||||
<div class="absolute bottom-16 left-0 w-28 h-[3px] opacity-0 group-hover:opacity-90 transition-opacity duration-300"
|
<div
|
||||||
style="background: linear-gradient(90deg, var(--color-heat), var(--color-pulse)); transform: skewX(-20deg) translateX(-4px);"></div>
|
class="absolute bottom-16 left-0 w-28 h-[3px] opacity-0 group-hover:opacity-90 transition-opacity duration-300"
|
||||||
|
style="background: linear-gradient(90deg, var(--color-heat), var(--color-pulse)); transform: skewX(-20deg) translateX(-4px);"
|
||||||
|
></div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="absolute bottom-0 left-0 right-0 p-5 md:p-6">
|
<div class="absolute bottom-0 left-0 right-0 p-5 md:p-6">
|
||||||
{{- with $post.Params.categories -}}
|
{{- with $post.Params.categories -}}
|
||||||
<div class="flex flex-wrap gap-2 mb-3">
|
<div class="flex flex-wrap gap-2 mb-3">
|
||||||
{{- range first 1 . -}}
|
{{- range first 1 . -}}
|
||||||
<span class="badge badge-outline">{{ . }}</span>
|
<span class="badge badge-outline">{{ . }}</span>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<h3 class="font-display text-xl md:text-2xl text-paper text-balance leading-none group-hover:text-heat transition-colors mb-3">
|
<h3
|
||||||
|
class="font-display text-xl md:text-2xl text-paper text-balance leading-none group-hover:text-heat transition-colors mb-3"
|
||||||
|
>
|
||||||
{{- $post.Title -}}
|
{{- $post.Title -}}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{{- with $post.Params.author -}}
|
{{- with $post.Params.author -}}
|
||||||
{{- $authorPage := site.GetPage (printf "authors/%s" .) -}}
|
{{- $authorPage := site.GetPage (printf "authors/%s" .) -}}
|
||||||
{{- with $authorPage -}}
|
{{- with $authorPage -}}
|
||||||
<span class="label text-chalk opacity-70">{{ .Params.name | default .Title }}</span>
|
<span class="label text-chalk opacity-70">{{ .Params.name | default .Title }}</span>
|
||||||
<span class="text-smoke opacity-60" aria-hidden="true">/</span>
|
<span class="text-smoke opacity-60" aria-hidden="true">/</span>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<time class="label text-chalk opacity-70" datetime="{{ $post.Date.Format "2006-01-02" }}">
|
<time class="label text-chalk opacity-70" datetime="{{ $post.Date.Format "2006-01-02" }}">
|
||||||
@@ -52,6 +58,5 @@
|
|||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -1,36 +1,41 @@
|
|||||||
{{- /* Compact post card for grids.
|
{{- /* Compact post card for grids.
|
||||||
Expects a post page as context (.).
|
Expects a post page as context (.).
|
||||||
*/ -}}
|
*/
|
||||||
|
-}}
|
||||||
{{- $post := . -}}
|
{{- $post := . -}}
|
||||||
<article class="group card-comic flex flex-col bg-concrete">
|
<article class="group card-comic flex flex-col bg-concrete">
|
||||||
<a href="{{ $post.RelPermalink }}" class="block flex-1 flex flex-col">
|
<a href="{{ $post.RelPermalink }}" class="block flex-1 flex flex-col">
|
||||||
|
|
||||||
<!-- Thumbnail — bundle banner.png → 01.png → .Params.banner -->
|
<!-- Thumbnail — bundle banner.png → 01.png → .Params.banner -->
|
||||||
{{- $thumb := $post.Resources.GetMatch "banner.png" -}}
|
{{- $thumb := $post.Resources.GetMatch "banner.png" -}}
|
||||||
{{- if not $thumb -}}{{- $thumb = $post.Resources.GetMatch "01.png" -}}{{- end -}}
|
{{- if not $thumb -}}{{- $thumb = $post.Resources.GetMatch "01.png" -}}{{- end -}}
|
||||||
<div class="card-media aspect-editorial">
|
<div class="card-media aspect-editorial">
|
||||||
{{- if $thumb -}}
|
{{- if $thumb -}}
|
||||||
{{- partial "img.html" (dict "res" $thumb "widths" (slice 600 1000) "sizes" "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw" "class" "w-full h-full object-cover" "alt" $post.Title) -}}
|
{{- partial "img.html" (dict "res" $thumb "widths" (slice 600 1000) "sizes" "(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw" "class" "w-full h-full object-cover" "alt" $post.Title) -}}
|
||||||
{{- else -}}{{- with $post.Params.banner -}}
|
|
||||||
{{- partial "media.html" (dict "media" . "class" "w-full h-full object-cover") -}}
|
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
<div class="w-full h-full flex items-center justify-center halftone">
|
{{- with $post.Params.banner -}}
|
||||||
<span class="label text-smoke text-xs">PIVOINE</span>
|
{{- partial "media.html" (dict "media" . "class" "w-full h-full object-cover") -}}
|
||||||
</div>
|
{{- else -}}
|
||||||
{{- end -}}{{- end -}}
|
<div class="w-full h-full flex items-center justify-center halftone">
|
||||||
|
<span class="label text-smoke text-xs">PIVOINE</span>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Category badge — parallelogram -->
|
<!-- Category badge — parallelogram -->
|
||||||
{{- with $post.Params.categories -}}
|
{{- with $post.Params.categories -}}
|
||||||
<div class="flex flex-wrap gap-2 px-4 pt-4">
|
<div class="flex flex-wrap gap-2 px-4 pt-4">
|
||||||
{{- range first 1 . -}}
|
{{- range . -}}
|
||||||
<span class="badge">{{ . }}</span>
|
<span class="badge">{{ . }}</span>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
|
|
||||||
<!-- Title -->
|
<!-- Title -->
|
||||||
<h3 class="font-display text-base md:text-lg text-paper text-balance leading-none group-hover:text-heat transition-colors px-4 pt-3 pb-2 flex-1">
|
<h3
|
||||||
|
class="font-display text-base md:text-lg text-paper text-balance leading-none group-hover:text-heat transition-colors px-4 pt-3 pb-2 flex-1"
|
||||||
|
>
|
||||||
{{- $post.Title -}}
|
{{- $post.Title -}}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@@ -42,10 +47,26 @@
|
|||||||
{{- $avRes := .Resources.GetMatch "avatar.*" -}}
|
{{- $avRes := .Resources.GetMatch "avatar.*" -}}
|
||||||
{{- if $avRes -}}
|
{{- if $avRes -}}
|
||||||
{{- $av := $avRes.Resize "64x webp" -}}
|
{{- $av := $avRes.Resize "64x webp" -}}
|
||||||
<img src="{{ $av.RelPermalink }}" alt="{{ $authorPage.Params.name | default $authorPage.Title }}" class="w-5 h-5 object-cover border border-smoke flex-shrink-0" width="{{ $av.Width }}" height="{{ $av.Height }}" loading="lazy" decoding="async">
|
<img
|
||||||
{{- else -}}{{- with .Params.avatar -}}
|
src="{{ $av.RelPermalink }}"
|
||||||
<img src="{{ . }}" alt="{{ $authorPage.Params.name | default $authorPage.Title }}" class="w-5 h-5 object-cover border border-smoke flex-shrink-0" loading="lazy" decoding="async">
|
alt="{{ $authorPage.Params.name | default $authorPage.Title }}"
|
||||||
{{- end -}}{{- end -}}
|
class="w-5 h-5 object-cover border border-smoke flex-shrink-0"
|
||||||
|
width="{{ $av.Width }}"
|
||||||
|
height="{{ $av.Height }}"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
{{- else -}}
|
||||||
|
{{- with .Params.avatar -}}
|
||||||
|
<img
|
||||||
|
src="{{ . }}"
|
||||||
|
alt="{{ $authorPage.Params.name | default $authorPage.Title }}"
|
||||||
|
class="w-5 h-5 object-cover border border-smoke flex-shrink-0"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
<span class="label text-fog">{{ .Params.name | default .Title }}</span>
|
<span class="label text-fog">{{ .Params.name | default .Title }}</span>
|
||||||
<span class="text-smoke" aria-hidden="true">/</span>
|
<span class="text-smoke" aria-hidden="true">/</span>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
@@ -54,6 +75,5 @@
|
|||||||
{{- $post.Date.Format "Jan 2006" -}}
|
{{- $post.Date.Format "Jan 2006" -}}
|
||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</a>
|
</a>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{{- else if and .IsPage (eq .Section "posts") -}}
|
{{- else if and .IsPage (eq .Section "posts") -}}
|
||||||
{{- $authorPage := "" -}}
|
{{- $authorPage := "" -}}
|
||||||
{{- with .Params.author -}}
|
{{- with .Params.author -}}
|
||||||
{{- $authorPage = site.GetPage (printf "authors/%s" .) -}}
|
{{- $authorPage = site.GetPage (printf "authors/%s" .) -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
{
|
{
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
|
|||||||
@@ -1,235 +1,183 @@
|
|||||||
{{- define "page-background" -}}
|
{{- define "page-background" -}}
|
||||||
{{- with .Params.background -}}
|
{{- with .Params.background -}}
|
||||||
<div class="fixed inset-0 -z-10 overflow-hidden" aria-hidden="true">
|
<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) -}}
|
{{- 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 class="absolute inset-0" style="background: rgba(5,5,16,0.7)"></div>
|
||||||
</div>
|
</div>
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|
||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
|
||||||
<!-- ── ARTICLE HEADER ────────────────────────────────────────── -->
|
<!-- ── ARTICLE HEADER ────────────────────────────────────────── -->
|
||||||
<header class="gutter-x pt-36 pb-12 max-w-5xl mx-auto relative overflow-hidden">
|
<header class="gutter-x pt-36 pb-12 relative overflow-hidden">
|
||||||
|
<div class="max-w-5xl mx-auto">
|
||||||
<!-- Faded decorative category text -->
|
<!-- Faded decorative category text -->
|
||||||
{{- with index .Params.categories 0 -}}
|
{{- with index .Params.categories 0 -}}
|
||||||
<div class="graffiti-tag absolute -top-2 -right-4 select-none pointer-events-none"
|
<div
|
||||||
style="-webkit-text-stroke-color: rgba(155,0,255,0.08);" aria-hidden="true">{{ . }}</div>
|
class="graffiti-tag absolute -top-2 -right-4 select-none pointer-events-none"
|
||||||
{{- end -}}
|
style="-webkit-text-stroke-color: rgba(155,0,255,0.08);"
|
||||||
|
aria-hidden="true"
|
||||||
<!-- Categories -->
|
>
|
||||||
{{- with .Params.categories -}}
|
{{ . }}
|
||||||
<div class="flex flex-wrap gap-2 mb-6">
|
</div>
|
||||||
{{- range . -}}
|
|
||||||
<a
|
|
||||||
href="/categories/{{ . | urlize }}/"
|
|
||||||
class="badge badge-outline"
|
|
||||||
>{{ . }}</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 -}}
|
|
||||||
<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") -}}
|
|
||||||
</div>
|
|
||||||
{{- else -}}{{- with .Params.banner -}}
|
|
||||||
<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) -}}
|
|
||||||
</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 -}}
|
|
||||||
|
|
||||||
{{- /* ── GENERATION NOTES (disabled) ─────────────────────────────
|
|
||||||
{{- with .Params.prompt -}}
|
|
||||||
<section class="gutter-x mb-20 md:mb-28">
|
|
||||||
<div class="max-w-5xl mx-auto">
|
|
||||||
<div class="flex items-center gap-5 mb-8">
|
|
||||||
<span class="badge badge-outline">Generation Notes</span>
|
|
||||||
<div class="flex-1 gradient-line"></div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-px bg-zinc">
|
|
||||||
{{- with .scene -}}
|
|
||||||
<div class="bg-concrete p-6 md:p-8">
|
|
||||||
<p class="label text-fog mb-4">Scene</p>
|
|
||||||
<p class="font-body text-sm text-chalk leading-relaxed">{{ . }}</p>
|
|
||||||
</div>
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
{{- with .environment -}}
|
|
||||||
<div class="bg-concrete p-6 md:p-8">
|
|
||||||
<p class="label text-fog mb-4">Environment</p>
|
|
||||||
<p class="font-body text-sm text-chalk leading-relaxed">{{ . }}</p>
|
|
||||||
</div>
|
|
||||||
{{- end -}}
|
|
||||||
{{- with .style -}}
|
|
||||||
<div class="bg-concrete p-6 md:p-8">
|
|
||||||
<p class="label text-fog mb-4">Lighting & Style</p>
|
|
||||||
<p class="font-body text-sm text-chalk leading-relaxed">{{ . }}</p>
|
|
||||||
</div>
|
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{- 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 -}}
|
<!-- Categories -->
|
||||||
{{- /* Pre-compute lightbox items so Alpine gets a static JSON array */ -}}
|
{{- with .Params.categories -}}
|
||||||
{{- $lbItems := slice -}}
|
<div class="flex flex-wrap gap-2 mb-6">
|
||||||
{{- range $bundleImages -}}
|
{{- range . -}}
|
||||||
{{- $stem := .Name | strings.TrimSuffix ".png" -}}
|
<a href="/categories/{{ . | urlize }}/" class="badge badge-outline">{{ . }}</a>
|
||||||
{{- $video := $.Resources.GetMatch (printf "%s.mp4" $stem) -}}
|
|
||||||
{{- $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="{ items: {{ $lbItems | jsonify }} }">
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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="$dispatch('lightbox:open', { items, idx: {{ $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 -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
</figure>
|
{{- 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 -->
|
||||||
|
<time class="label text-fog" datetime="{{ .Date.Format "2006-01-02" }}">
|
||||||
|
{{- .Date.Format "January 2, 2006" -}}
|
||||||
|
</time>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- ── GRADIENT DIVIDER ──────────────────────────────────────── -->
|
||||||
|
<div class="gradient-line mb-0"></div>
|
||||||
|
|
||||||
|
<!-- ── BANNER MEDIA — video if available, lightbox on click ──── -->
|
||||||
|
{{- $bannerImg := .Resources.GetMatch "banner.png" -}}
|
||||||
|
{{- if not $bannerImg -}}{{- $bannerImg = .Resources.GetMatch "01.png" -}}{{- end -}}
|
||||||
|
{{- if $bannerImg -}}
|
||||||
|
{{- $stem := $bannerImg.Name | strings.TrimSuffix ".png" -}}
|
||||||
|
{{- $bannerVid := $.Resources.GetMatch (printf "%s.mp4" $stem) -}}
|
||||||
|
{{- $webp := $bannerImg.Resize "1200x webp" -}}
|
||||||
|
{{- $lbItems := slice (dict "img" $webp.RelPermalink "video" "") -}}
|
||||||
|
{{- if $bannerVid -}}
|
||||||
|
{{- $lbItems = $lbItems | append (dict "img" $webp.RelPermalink "video" $bannerVid.RelPermalink) -}}
|
||||||
|
{{- end -}}
|
||||||
|
<div
|
||||||
|
class="w-full overflow-hidden mb-16 md:mb-24 cursor-zoom-in"
|
||||||
|
style="height: clamp(320px, 60vh, 75vh)"
|
||||||
|
x-data="{ items: {{ $lbItems | jsonify }} }"
|
||||||
|
@click="$dispatch('lightbox:open', { items, idx: 0 })"
|
||||||
|
>
|
||||||
|
{{- if $bannerVid -}}
|
||||||
|
<video
|
||||||
|
class="w-full h-full object-cover object-center pointer-events-none"
|
||||||
|
src="{{ $bannerVid.RelPermalink }}"
|
||||||
|
poster="{{ $bannerImg.RelPermalink }}"
|
||||||
|
autoplay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
></video>
|
||||||
|
{{- else -}}
|
||||||
|
{{- partial "img.html" (dict "res" $bannerImg "widths" (slice 1200 1800) "sizes" "100vw" "class" "w-full h-full object-cover object-center pointer-events-none" "alt" .Title "loading" "eager") -}}
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</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 -}}
|
{{- end -}}
|
||||||
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
<!-- ── TAGS ─────────────────────────────────────────────────── -->
|
<!-- ── PROSE BODY ────────────────────────────────────────────── -->
|
||||||
{{- with .Params.tags -}}
|
{{- with .Content -}}
|
||||||
<footer class="gutter-x border-t border-zinc pt-10 pb-20">
|
<article class="gutter-x mb-20 md:mb-28">
|
||||||
<div class="max-w-5xl mx-auto flex flex-wrap gap-3 items-center">
|
<div class="max-w-5xl mx-auto">
|
||||||
<span class="label text-fog">Tagged:</span>
|
<div class="max-w-3xl prose-editorial">
|
||||||
{{- range . -}}
|
{{ . }}
|
||||||
<a
|
</div>
|
||||||
href="/tags/{{ . | urlize }}/"
|
</div>
|
||||||
class="badge badge-outline"
|
</article>
|
||||||
># {{ . }}</a>
|
{{- end -}}
|
||||||
{{- end -}}
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
{{- end -}}
|
|
||||||
|
|
||||||
<!-- ── RELATED POSTS ─────────────────────────────────────────── -->
|
|
||||||
{{- $related := .Site.RegularPages.Related . | first 3 -}}
|
<!-- ── GENERATION NOTES (disabled — remove comment tags to enable)
|
||||||
{{- with $related -}}
|
{{- with .Params.prompt -}}
|
||||||
<section class="bg-concrete border-t border-zinc gutter-x py-16 md:py-24 spray-bg">
|
<section class="gutter-x mb-20 md:mb-28">
|
||||||
<div class="max-w-5xl mx-auto">
|
<div class="max-w-5xl mx-auto">
|
||||||
<div class="flex items-center gap-5 mb-10">
|
<div class="flex items-center gap-5 mb-8">
|
||||||
<h2 class="font-display text-3xl md:text-4xl text-paper leading-none">
|
<span class="badge badge-outline">Generation Notes</span>
|
||||||
More <span class="text-gradient">Drops</span>
|
<div class="flex-1 gradient-line"></div>
|
||||||
</h2>
|
</div>
|
||||||
<div class="flex-1 gradient-line ml-2"></div>
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-px bg-zinc">
|
||||||
</div>
|
{{- with .scene -}}
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-5">
|
<div class="bg-concrete p-6 md:p-8">
|
||||||
{{- range . -}}
|
<p class="label text-fog mb-4">Scene</p>
|
||||||
{{- partial "post-card.html" . -}}
|
<p class="font-body text-sm text-chalk leading-relaxed">{{ . }}</p>
|
||||||
{{- end -}}
|
</div>
|
||||||
</div>
|
{{- end -}}
|
||||||
</div>
|
{{- with .environment -}}
|
||||||
</section>
|
<div class="bg-concrete p-6 md:p-8">
|
||||||
{{- end -}}
|
<p class="label text-fog mb-4">Environment</p>
|
||||||
|
<p class="font-body text-sm text-chalk leading-relaxed">{{ . }}</p>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
{{- with .style -}}
|
||||||
|
<div class="bg-concrete p-6 md:p-8">
|
||||||
|
<p class="label text-fog mb-4">Lighting & Style</p>
|
||||||
|
<p class="font-body text-sm text-chalk leading-relaxed">{{ . }}</p>
|
||||||
|
</div>
|
||||||
|
{{- end -}}
|
||||||
|
</div>
|
||||||
|
</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 no-wrap gap-3 items-start">
|
||||||
|
<span class="label text-fog">Tagged:</span>
|
||||||
|
<span class="flex flex-wrap gap-3">
|
||||||
|
{{- range . -}}
|
||||||
|
<a href="/tags/{{ . | urlize }}/" class="badge badge-outline"># {{ . }}</a>
|
||||||
|
{{- end -}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ── RELATED POSTS ─────────────────────────────────────────── -->
|
||||||
|
{{- $related := .Site.RegularPages.Related . | first 3 -}}
|
||||||
|
{{- with $related -}}
|
||||||
|
<section class="bg-concrete border-t border-zinc gutter-x py-16 md:py-24 spray-bg">
|
||||||
|
<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 -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
{{- define "main" -}}
|
{{- define "main" -}}
|
||||||
|
<!-- Tag header -->
|
||||||
|
<header class="gutter-x pt-36 pb-16 border-b border-mist">
|
||||||
|
<div class="max-w-4xl">
|
||||||
|
<span class="label text-ash block mb-4">Tag</span>
|
||||||
|
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance"># {{ .Title }}</h1>
|
||||||
|
<p class="text-ash label mt-5">
|
||||||
|
{{ len .Pages }} {{ if eq (len .Pages) 1 }}editorial{{ else }}editorials{{ end }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<!-- Tag header -->
|
<!-- Posts grid -->
|
||||||
<header class="gutter-x pt-36 pb-16 border-b border-mist">
|
<section class="gutter-x py-16 md:py-24">
|
||||||
<div class="max-w-4xl">
|
{{- if .Paginator.Pages -}}
|
||||||
<span class="label text-ash block mb-4">Tag</span>
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-5 gap-y-12">
|
||||||
<h1 class="font-display text-5xl md:text-7xl text-paper text-balance"># {{ .Title }}</h1>
|
{{- range .Paginator.Pages -}}
|
||||||
<p class="text-ash label mt-5">{{ len .Pages }} {{ if eq (len .Pages) 1 }}editorial{{ else }}editorials{{ end }}</p>
|
{{- partial "post-card.html" . -}}
|
||||||
</div>
|
{{- end -}}
|
||||||
</header>
|
</div>
|
||||||
|
{{- partial "pagination.html" . -}}
|
||||||
<!-- Posts grid -->
|
{{- else -}}
|
||||||
<section class="gutter-x py-16 md:py-24">
|
<p class="text-ash label text-center py-24">No posts with this tag yet.</p>
|
||||||
{{- if .Paginator.Pages -}}
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-5 gap-y-12">
|
|
||||||
{{- range .Paginator.Pages -}}
|
|
||||||
{{- partial "post-card.html" . -}}
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
</div>
|
</section>
|
||||||
{{- partial "pagination.html" . -}}
|
|
||||||
{{- else -}}
|
|
||||||
<p class="text-ash label text-center py-24">No posts with this tag yet.</p>
|
|
||||||
{{- end -}}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{{- end -}}
|
{{- end -}}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ server {
|
|||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml application/json application/rss+xml image/svg+xml;
|
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml application/json application/rss+xml image/svg+xml;
|
||||||
|
|
||||||
|
open_file_cache max=1000 inactive=30s;
|
||||||
|
open_file_cache_valid 60s;
|
||||||
|
open_file_cache_min_uses 2;
|
||||||
|
open_file_cache_errors on;
|
||||||
|
|
||||||
# Security headers
|
# Security headers
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
|||||||
@@ -4,13 +4,15 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "hugo server --buildDrafts --disableFastRender",
|
"dev": "hugo server --buildDrafts --disableFastRender",
|
||||||
"build": "NODE_ENV=production hugo --minify"
|
"build": "NODE_ENV=production hugo --minify",
|
||||||
|
"format": "prettier --write \"layouts/**/*.html\" \"assets/**/*.css\" \"assets/**/*.js\" \"content/**/*.md\" \"*.toml\"",
|
||||||
|
"format:check": "prettier --check \"layouts/**/*.html\" \"assets/**/*.css\" \"assets/**/*.js\" \"content/**/*.md\" \"*.toml\""
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tailwindcss": "^4.0.0",
|
"@tailwindcss/cli": "^4.0.0",
|
||||||
"@tailwindcss/postcss": "^4.0.0",
|
"prettier": "^3.8.3",
|
||||||
"autoprefixer": "^10.4.0",
|
"prettier-plugin-go-template": "^0.0.15",
|
||||||
"postcss": "^8.5.0",
|
"prettier-plugin-toml": "^2.0.6",
|
||||||
"postcss-cli": "^11.0.0"
|
"tailwindcss": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
preferSymlinkedExecutables: true
|
||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
"@tailwindcss/postcss": {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Generate a post's banner video (01.mp4) and preview image (01.png) via
|
||||||
|
# the Replicate black-forest-labs/flux-3 text-to-video model, using the
|
||||||
|
# prompt.{scene,environment,style,video} fields already present in the
|
||||||
|
# post's frontmatter.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# sh scripts/create-video.sh <path-to-post-dir-or-index.md> [--force] [--dry-run]
|
||||||
|
#
|
||||||
|
# Requires: REPLICATE_API_TOKEN exported in the environment.
|
||||||
|
#
|
||||||
|
# Video settings (matching every existing post's square/silent convention,
|
||||||
|
# just at higher resolution): 1:1 aspect ratio, 1080p, 5s, no audio.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
print_usage() {
|
||||||
|
echo "Usage: sh scripts/create-video.sh <post-dir-or-index.md> [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Generates a post's banner video (01.mp4) via the Replicate flux-3"
|
||||||
|
echo "text-to-video model, using that post's prompt.{scene,environment,style,video}"
|
||||||
|
echo "frontmatter fields, then extracts the first frame as 01.png."
|
||||||
|
echo ""
|
||||||
|
echo "Arguments:"
|
||||||
|
echo " <post-dir-or-index.md> Path to content/posts/<slug>/ or its index.md"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " -f, --force Overwrite existing 01.mp4 / 01.png"
|
||||||
|
echo " --dry-run Print the constructed prompt and request JSON, make no API call"
|
||||||
|
echo " -h, --help Show this help"
|
||||||
|
echo ""
|
||||||
|
echo "Requires REPLICATE_API_TOKEN to be exported in the environment."
|
||||||
|
}
|
||||||
|
|
||||||
|
FORCE=0
|
||||||
|
DRY_RUN=0
|
||||||
|
POST_ARG=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
-f|--force) FORCE=1 ;;
|
||||||
|
--dry-run) DRY_RUN=1 ;;
|
||||||
|
-h|--help) print_usage; exit 0 ;;
|
||||||
|
-*)
|
||||||
|
echo "Unknown option: $arg" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*) POST_ARG="$arg" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$POST_ARG" ]; then
|
||||||
|
print_usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "$POST_ARG" ]; then
|
||||||
|
BUNDLE_DIR="${POST_ARG%/}"
|
||||||
|
INDEX_MD="$BUNDLE_DIR/index.md"
|
||||||
|
else
|
||||||
|
INDEX_MD="$POST_ARG"
|
||||||
|
BUNDLE_DIR=$(dirname "$POST_ARG")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$INDEX_MD" ]; then
|
||||||
|
echo "Error: $INDEX_MD not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$REPLICATE_API_TOKEN" ]; then
|
||||||
|
echo "Error: REPLICATE_API_TOKEN is not set in your environment." >&2
|
||||||
|
echo "Export it before running this script, e.g.:" >&2
|
||||||
|
echo " export REPLICATE_API_TOKEN=r8_..." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEST_MP4="$BUNDLE_DIR/01.mp4"
|
||||||
|
DEST_PNG="$BUNDLE_DIR/01.png"
|
||||||
|
|
||||||
|
if [ "$FORCE" -ne 1 ]; then
|
||||||
|
if [ -e "$DEST_MP4" ]; then
|
||||||
|
echo "Error: $DEST_MP4 already exists (use --force to overwrite)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -e "$DEST_PNG" ]; then
|
||||||
|
echo "Error: $DEST_PNG already exists (use --force to overwrite)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
PROMPT_JSON=$(yq --front-matter=extract -o=json '.prompt' "$INDEX_MD")
|
||||||
|
|
||||||
|
if [ "$PROMPT_JSON" = "null" ] || [ -z "$PROMPT_JSON" ]; then
|
||||||
|
echo "Error: $INDEX_MD has no prompt.* fields in its frontmatter" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCENE=$(printf '%s' "$PROMPT_JSON" | jq -r '.scene // empty')
|
||||||
|
ENVIRONMENT=$(printf '%s' "$PROMPT_JSON" | jq -r '.environment // empty')
|
||||||
|
STYLE=$(printf '%s' "$PROMPT_JSON" | jq -r '.style // empty')
|
||||||
|
VIDEO=$(printf '%s' "$PROMPT_JSON" | jq -r '.video // empty')
|
||||||
|
|
||||||
|
if [ -z "$SCENE" ]; then
|
||||||
|
echo "Error: prompt.scene is missing/empty in $INDEX_MD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$ENVIRONMENT" ]; then
|
||||||
|
echo "Error: prompt.environment is missing/empty in $INDEX_MD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$STYLE" ]; then
|
||||||
|
echo "Error: prompt.style is missing/empty in $INDEX_MD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$VIDEO" ]; then
|
||||||
|
echo "Error: prompt.video is missing/empty in $INDEX_MD" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SLUG=$(yq --front-matter=extract '.slug' "$INDEX_MD")
|
||||||
|
echo "▶ ${SLUG:-$(basename "$BUNDLE_DIR")}: building prompt from frontmatter"
|
||||||
|
|
||||||
|
PROMPT="${SCENE} ${ENVIRONMENT} ${STYLE} ${VIDEO}"
|
||||||
|
|
||||||
|
INPUT_JSON=$(jq -n \
|
||||||
|
--arg prompt "$PROMPT" \
|
||||||
|
--arg duration "5" \
|
||||||
|
--arg resolution "1080p" \
|
||||||
|
--arg aspect_ratio "1:1" \
|
||||||
|
'{
|
||||||
|
input: {
|
||||||
|
prompt: $prompt,
|
||||||
|
images: [],
|
||||||
|
duration: $duration,
|
||||||
|
resolution: $resolution,
|
||||||
|
aspect_ratio: $aspect_ratio,
|
||||||
|
generate_audio: false
|
||||||
|
}
|
||||||
|
}')
|
||||||
|
|
||||||
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "--- Constructed prompt ---"
|
||||||
|
echo "$PROMPT"
|
||||||
|
echo "--- Request body ---"
|
||||||
|
echo "$INPUT_JSON" | jq .
|
||||||
|
echo "(dry run — no API call made)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "▶ Submitting generation request to Replicate (flux-3)..."
|
||||||
|
CREATE_RESPONSE=$(curl -sS \
|
||||||
|
-X POST \
|
||||||
|
-H "Authorization: Bearer $REPLICATE_API_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Prefer: wait=60" \
|
||||||
|
-d "$INPUT_JSON" \
|
||||||
|
https://api.replicate.com/v1/models/black-forest-labs/flux-3/predictions \
|
||||||
|
-w '\n%{http_code}')
|
||||||
|
|
||||||
|
HTTP_CODE=$(printf '%s' "$CREATE_RESPONSE" | tail -n1)
|
||||||
|
BODY=$(printf '%s' "$CREATE_RESPONSE" | sed '$d')
|
||||||
|
|
||||||
|
case "$HTTP_CODE" in
|
||||||
|
2??) : ;;
|
||||||
|
*)
|
||||||
|
echo "Error: Replicate API returned HTTP $HTTP_CODE" >&2
|
||||||
|
echo "$BODY" | jq -r '.detail // .error // .' >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
STATUS=$(printf '%s' "$BODY" | jq -r '.status')
|
||||||
|
GET_URL=$(printf '%s' "$BODY" | jq -r '.urls.get')
|
||||||
|
PREDICTION_ID=$(printf '%s' "$BODY" | jq -r '.id')
|
||||||
|
POLL_BODY="$BODY"
|
||||||
|
|
||||||
|
if [ -z "$GET_URL" ] || [ "$GET_URL" = "null" ]; then
|
||||||
|
echo "Error: unexpected response from Replicate (no urls.get):" >&2
|
||||||
|
echo "$BODY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
POLL_INTERVAL=5
|
||||||
|
MAX_WAIT=600
|
||||||
|
ELAPSED=0
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
case "$STATUS" in
|
||||||
|
succeeded) break ;;
|
||||||
|
failed|canceled)
|
||||||
|
ERR=$(printf '%s' "$POLL_BODY" | jq -r '.error // "unknown error"')
|
||||||
|
echo "Error: prediction $STATUS: $ERR" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then
|
||||||
|
echo "Error: timed out after ${MAX_WAIT}s waiting on prediction $PREDICTION_ID" >&2
|
||||||
|
echo "Check status manually: curl -H \"Authorization: Bearer \$REPLICATE_API_TOKEN\" $GET_URL" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$POLL_INTERVAL"
|
||||||
|
ELAPSED=$((ELAPSED + POLL_INTERVAL))
|
||||||
|
|
||||||
|
POLL_BODY=$(curl -sS -H "Authorization: Bearer $REPLICATE_API_TOKEN" "$GET_URL")
|
||||||
|
STATUS=$(printf '%s' "$POLL_BODY" | jq -r '.status')
|
||||||
|
echo " … ${ELAPSED}s elapsed, status: $STATUS"
|
||||||
|
done
|
||||||
|
|
||||||
|
OUTPUT_URL=$(printf '%s' "$POLL_BODY" | jq -r '.output')
|
||||||
|
if [ -z "$OUTPUT_URL" ] || [ "$OUTPUT_URL" = "null" ]; then
|
||||||
|
echo "Error: prediction succeeded but no output URL was returned" >&2
|
||||||
|
echo "$POLL_BODY" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/create-video.XXXXXX")
|
||||||
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||||
|
|
||||||
|
TMP_MP4="$TMP_DIR/01.mp4"
|
||||||
|
TMP_PNG="$TMP_DIR/01.png"
|
||||||
|
|
||||||
|
echo "▶ Downloading generated video..."
|
||||||
|
curl -fSL -o "$TMP_MP4" "$OUTPUT_URL"
|
||||||
|
|
||||||
|
echo "▶ Extracting first frame as preview PNG..."
|
||||||
|
ffmpeg -y -loglevel error -i "$TMP_MP4" -vframes 1 "$TMP_PNG" < /dev/null
|
||||||
|
|
||||||
|
mv "$TMP_MP4" "$DEST_MP4"
|
||||||
|
mv "$TMP_PNG" "$DEST_PNG"
|
||||||
|
|
||||||
|
echo "✓ Wrote $DEST_MP4 and $DEST_PNG"
|
||||||