Refactor: replace all BEM CSS with Tailwind utility classes

Remove all BEM component classes (hero__*, card__*, issue-card__*, foot__*,
masthead__*, searchpop__*, lb__*) from CSS and templates. Replace with
Tailwind v4 utility classes inline in HTML. Create card.html partial to
avoid repeating verbose utility strings across grid templates. Rename
lightbox CSS to flat lb-* and search popup to sp-*.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 18:06:55 +02:00
parent e3e9cf6742
commit 54a87dc4ed
15 changed files with 590 additions and 604 deletions
+84 -236
View File
@@ -53,206 +53,72 @@ body {
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
img { display: block; max-width: 100%; } img { display: block; max-width: 100%; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; } button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; } a { color: inherit; text-decoration: none; }
::selection { background: var(--ink); color: var(--paper); } ::selection { background: var(--ink); color: var(--paper); }
/* ── paper grain overlay ── */ /* ── paper grain overlay ── */
body::before { body::before {
content: ""; content: "";
position: fixed; inset: 0; position: fixed; inset: 0;
pointer-events: none; pointer-events: none; z-index: 1; opacity: .06; mix-blend-mode: multiply;
z-index: 1;
opacity: .06;
mix-blend-mode: multiply;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>"); background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.7 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
} }
/* ── masthead ── */ /* ── nav link animated underline ── */
.masthead { .nav-link { position: relative; padding-bottom: 2px; transition: color .2s; }
position: sticky; top: 0; z-index: 50; .nav-link::after {
backdrop-filter: blur(14px) saturate(1.05);
background: color-mix(in oklab, var(--paper) 86%, transparent);
border-bottom: 1px solid var(--rule);
}
.masthead__inner {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
gap: clamp(12px, 2vw, 24px);
padding: 14px var(--pad);
}
.masthead__left, .masthead__right {
display: flex; align-items: center; gap: 18px;
font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
color: var(--ink-soft);
min-width: 0;
}
.masthead__left > *, .masthead__right > * { white-space: nowrap; }
.mh-link { position: relative; padding-bottom: 2px; transition: color .2s; }
.mh-link::after {
content: ""; position: absolute; left: 0; right: 0; bottom: 0; content: ""; position: absolute; left: 0; right: 0; bottom: 0;
height: 1px; background: currentColor; transform: scaleX(.5); transform-origin: left; height: 1px; background: currentColor;
transform: scaleX(.5); transform-origin: left;
transition: transform .25s, background .2s; transition: transform .25s, background .2s;
} }
.mh-link:hover { color: var(--ink); } .nav-link:hover { color: var(--ink); }
.mh-link:hover::after { transform: scaleX(1); background: var(--roux); } .nav-link:hover::after { transform: scaleX(1); background: var(--roux); }
.masthead__right { justify-content: flex-end; }
.masthead__date { font-variant-numeric: tabular-nums; }
.masthead__logo { display: block; line-height: 1; color: var(--ink); justify-self: center; }
.logo { display: grid; grid-template-columns: auto; justify-items: center; gap: 4px; text-align: center; } /* ── card animation (uses --d CSS custom property for stagger) ── */
.logo__mark { width: 30px; height: 30px; display: block; } @keyframes rouxIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.logo__word { font-family: var(--display); font-weight: 400; font-size: 30px; line-height: 1; letter-spacing: .045em; color: var(--ink); } .card { animation: rouxIn .6s cubic-bezier(.2,.7,.2,1) both; animation-delay: var(--d, 0ms); }
.logo__tag { font: 500 8.5px/1 var(--sans); letter-spacing: .32em; text-transform: uppercase; color: var(--ink-soft); } .card:hover .card-img { transform: scale(1.025); }
@media (max-width: 620px) { /* ── issue card animation ── */
.logo__mark { width: 22px; height: 22px; } [data-issue-card] {
.logo__word { font-size: 22px; } opacity: 0; transform: translateY(8px);
.logo__tag { font-size: 7.5px; letter-spacing: .28em; } animation: rouxIn .65s cubic-bezier(.2,.7,.2,1) forwards;
animation-delay: var(--d, 0ms);
} }
@media (max-width: 1100px) { .masthead__left .mh-issue, .masthead__right .mh-city { display: none; } } @supports (animation-timeline: view()) { [data-issue-card] { opacity: 1; transform: none; animation: none; } }
@media (max-width: 820px) { .masthead__left .mh-pub, .masthead__left .mh-sep, .masthead__right .mh-sep { display: none; } }
@media (max-width: 620px) { .masthead__inner { padding: 12px var(--pad); } .masthead__left { font-size: 10px; gap: 10px; } .masthead__right { font-size: 10px; gap: 10px; } }
@media (max-width: 460px) { .masthead__inner { grid-template-columns: 1fr; } .masthead__left, .masthead__right { display: none; } .masthead__logo { justify-self: center; } }
/* subhead: search bar */ /* ── tabs: JS drives aria-pressed, CSS drives the active look ── */
.subhead {
display: flex; align-items: stretch;
border-top: 1px solid var(--rule-2); border-bottom: 1px solid var(--rule);
background: color-mix(in oklab, var(--paper) 95%, transparent);
position: relative;
}
.subhead__search {
flex: 1; display: flex; align-items: center; gap: 12px;
padding: 10px var(--pad); border-right: 1px solid var(--rule-2); min-width: 0;
}
.subhead__search input {
flex: 1; min-width: 0; border: 0; background: transparent; outline: none;
font: 400 16px/1 var(--serif); font-style: italic; color: var(--ink); letter-spacing: .005em; padding: 6px 0;
}
.subhead__search input::placeholder { color: var(--ink-soft); opacity: .9; }
.subhead__kbd { flex: 0 0 auto; font: 500 10px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; border: 1px solid var(--rule); padding: 5px 7px; border-radius: 3px; color: var(--ink-soft); }
.subhead__count { display: flex; align-items: center; padding: 0 var(--pad); font: 500 11px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: var(--ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }
.subhead__count b { color: var(--ink); font-weight: 600; margin-right: 4px; }
@media (max-width: 720px) { .subhead__count { display: none; } .subhead__kbd { display: none; } .subhead__search input { font-size: 15px; } }
/* tabs */
.tabs { display: flex; gap: 4px; overflow-x: auto; padding: 10px var(--pad); border-bottom: 1px solid var(--rule-2); background: var(--paper); scrollbar-width: none; }
.tabs::-webkit-scrollbar { display: none; }
.tabs button { flex: 0 0 auto; padding: 8px 14px; border-radius: 999px; font: 500 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft); border: 1px solid transparent; transition: color .2s, border-color .2s, background .2s; }
.tabs button:hover { color: var(--ink); }
.tabs button[aria-pressed="true"] { color: var(--paper); background: var(--ink); border-color: var(--ink); } .tabs button[aria-pressed="true"] { color: var(--paper); background: var(--ink); border-color: var(--ink); }
/* ── hero strip ── */ /* ── search highlight ── */
.hero {
padding: clamp(40px, 8vw, 96px) var(--pad) clamp(28px, 5vw, 64px);
display: grid; grid-template-columns: 1fr; gap: 18px;
border-bottom: 1px solid var(--rule);
}
.hero__eyebrow { font: 500 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: var(--ink-soft); display: flex; align-items: center; gap: 12px; }
.hero__eyebrow::before { content: ""; height: 1px; width: 36px; background: var(--ink-soft); }
.hero__title { margin: 0; font: 400 clamp(48px, 9vw, 132px)/0.92 var(--display); letter-spacing: -0.01em; }
.hero__title em { font-family: var(--serif); font-style: italic; font-weight: 300; color: var(--roux); }
.hero__lede { max-width: 56ch; font: 400 clamp(16px, 1.4vw, 19px)/1.5 var(--serif); color: var(--ink-2); margin-top: 6px; }
.hero__lede em { color: var(--roux); font-style: italic; }
/* ── photo grid ── */
.grid {
padding: clamp(20px, 3vw, 40px) var(--pad) 80px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: var(--gap);
row-gap: clamp(40px, 5vw, 72px);
}
@media (max-width: 1200px) { .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 820px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 520px) { .grid { grid-template-columns: 1fr; } }
.grid[data-density="cozy"] { grid-template-columns: repeat(3, minmax(0, 1fr)); row-gap: 88px; }
.grid[data-density="compact"] { grid-template-columns: repeat(5, minmax(0, 1fr)); row-gap: 56px; }
@media (max-width: 1200px) { .grid[data-density="cozy"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } .grid[data-density="compact"] { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 820px) { .grid[data-density="cozy"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } .grid[data-density="compact"] { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 520px) { .grid[data-density="cozy"] { grid-template-columns: 1fr; } .grid[data-density="compact"] { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* card */
.card { display: block; cursor: pointer; }
.card__frame { position: relative; aspect-ratio: 2/3; overflow: hidden; background: var(--paper-2); border-radius: 1px; }
.card__frame::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(22,17,13,.04); pointer-events: none; }
.card__img { width: 100%; height: 100%; object-fit: cover; transition: transform .9s cubic-bezier(.2,.7,.1,1), filter .6s; filter: saturate(.92) contrast(1.02); }
.card:hover .card__img { transform: scale(1.025); }
.card__num { position: absolute; top: 10px; left: 10px; font: 500 10px/1 var(--sans); letter-spacing: .18em; color: var(--paper); mix-blend-mode: difference; font-variant-numeric: tabular-nums; }
.card__cat { position: absolute; top: 10px; right: 10px; font: 500 10px/1 var(--sans); letter-spacing: .18em; text-transform: uppercase; color: var(--paper); mix-blend-mode: difference; }
.card__issue { position: absolute; bottom: 10px; left: 10px; font: 500 10px/1 var(--sans); letter-spacing: .18em; color: var(--paper); mix-blend-mode: difference; font-variant-numeric: tabular-nums; }
.card__meta { margin-top: 14px; display: grid; gap: 4px; }
.card__title { font: 400 clamp(20px, 1.6vw, 26px)/1.1 var(--display); letter-spacing: 0; margin: 0; }
.card__title em { font-family: var(--serif); font-style: italic; color: var(--roux); }
.card__sub { font: 400 12px/1.4 var(--sans); letter-spacing: .04em; color: var(--ink-soft); display: flex; align-items: center; gap: 10px; }
.card__sub .dot { width: 3px; height: 3px; background: var(--ink-soft); border-radius: 50%; opacity: .5; }
.card__desc { font: 400 13px/1.5 var(--serif); color: var(--ink-2); font-style: italic; margin-top: 2px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
/* highlight matches */
mark.hl { background: color-mix(in oklab, var(--roux) 22%, transparent); color: inherit; padding: 0 1px; border-radius: 1px; } mark.hl { background: color-mix(in oklab, var(--roux) 22%, transparent); color: inherit; padding: 0 1px; border-radius: 1px; }
/* ── issues archive ── */ /* ── search popup (position + display driven by JS) ── */
.issues-grid { .searchpop {
padding: clamp(20px, 3vw, 40px) var(--pad) 80px; position: absolute; top: calc(100% - 1px); left: 0; right: 0;
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); background: var(--paper); border: 1px solid var(--rule); border-top: 0;
column-gap: clamp(24px, 3vw, 48px); row-gap: clamp(36px, 5vw, 64px); max-height: min(70vh, 620px); overflow: auto; display: none; z-index: 60;
box-shadow: 0 24px 60px -30px rgba(22,17,13,.35);
} }
@media (max-width: 820px) { .issues-grid { grid-template-columns: 1fr; } }
.issue-card {
display: grid; grid-template-columns: 240px 1fr;
gap: clamp(20px, 2.4vw, 36px); align-items: start;
color: inherit;
padding-bottom: clamp(28px, 3.5vw, 44px); border-bottom: 1px solid var(--rule-2);
}
@media (max-width: 560px) { .issue-card { grid-template-columns: 160px 1fr; } }
.issue-card--forthcoming { color: var(--ink-soft); cursor: default; }
.issue-card__cover { aspect-ratio: 2/3; overflow: hidden; background: var(--paper-2); position: relative; }
.issue-card__cover::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(22,17,13,.04); }
.issue-card__cover img { width: 100%; height: 100%; object-fit: cover; transition: transform .9s cubic-bezier(.2,.7,.1,1); }
.issue-card:not(.issue-card--forthcoming):hover .issue-card__cover img { transform: scale(1.025); }
.issue-card__forth { position: absolute; inset: 0; display: grid; place-items: center; align-content: center; gap: 14px; color: var(--ink-soft); font: 500 10px/1 var(--sans); letter-spacing: .22em; text-transform: uppercase; border: 1px dashed var(--rule); margin: 12px; }
.issue-card__forth svg { opacity: .5; }
.issue-card__meta { padding-top: 6px; display: flex; flex-direction: column; align-items: flex-start; gap: 14px; min-width: 0; }
.issue-card__num { font: 500 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: var(--ink-soft); }
.issue-card__title { margin: 0; font: 400 clamp(28px, 3vw, 44px)/1.0 var(--display); letter-spacing: -0.005em; }
.issue-card__title em { font-family: var(--serif); font-style: italic; color: var(--roux); font-weight: 300; }
.issue-card--forthcoming .issue-card__title { color: var(--ink-soft); }
.issue-card__blurb { font: 400 14.5px/1.55 var(--serif); font-style: italic; color: var(--ink-2); margin: 0; max-width: 52ch; }
.issue-card__foot { margin-top: 4px; display: flex; align-items: baseline; gap: 18px; flex-wrap: wrap; font: 500 10.5px/1.4 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-soft); width: 100%; }
.issue-card__foot > span:first-child { flex: 1; min-width: 0; }
.issue-card__cta { font-weight: 600; color: var(--ink); border-bottom: 1px solid var(--ink); padding-bottom: 2px; letter-spacing: .14em; }
.issue-card__cta--muted { color: var(--ink-soft); border-bottom-color: var(--rule); }
/* ── empty state ── */
.empty { grid-column: 1/-1; padding: 80px 0; text-align: center; color: var(--ink-soft); }
.empty h3 { font: 400 28px/1.2 var(--display); color: var(--ink); margin: 0 0 8px; }
.empty button { margin-top: 16px; border: 1px solid var(--ink); padding: 9px 18px; font: 500 11px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; }
.empty button:hover { background: var(--ink); color: var(--paper); }
/* ── search popup ── */
.searchpop { position: absolute; top: calc(100% - 1px); left: 0; right: 0; background: var(--paper); border: 1px solid var(--rule); border-top: 0; max-height: min(70vh, 620px); overflow: auto; display: none; z-index: 60; box-shadow: 0 24px 60px -30px rgba(22,17,13,.35); }
.searchpop[data-open="true"] { display: block; } .searchpop[data-open="true"] { display: block; }
.searchpop__section { padding: 12px var(--pad); border-bottom: 1px solid var(--rule-2); } .sp-section { padding: 12px var(--pad); border-bottom: 1px solid var(--rule-2); }
.searchpop__section:last-child { border-bottom: 0; } .sp-section:last-child { border-bottom: 0; }
.searchpop__label { font: 500 10px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; } .sp-label { font: 500 10px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: var(--ink-soft); margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }
.searchpop__label small { font-family: var(--mono); letter-spacing: 0; font-size: 10px; } .sp-label small { font-family: var(--mono); letter-spacing: 0; font-size: 10px; }
.searchpop__chips { display: flex; flex-wrap: wrap; gap: 6px; } .sp-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.searchpop__chip { padding: 5px 10px; border: 1px solid var(--rule); border-radius: 999px; font: 500 11px/1 var(--sans); letter-spacing: .06em; color: var(--ink-2); } .sp-chip { padding: 5px 10px; border: 1px solid var(--rule); border-radius: 999px; font: 500 11px/1 var(--sans); letter-spacing: .06em; color: var(--ink-2); }
.searchpop__chip:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); } .sp-chip:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.searchpop__chip b { color: var(--roux); font-weight: 600; } .sp-chip b { color: var(--roux); font-weight: 600; }
.searchpop__hits { display: grid; gap: 2px; } .sp-hits { display: grid; gap: 2px; }
.searchpop__hit { display: grid; grid-template-columns: 44px 1fr auto; gap: 14px; align-items: center; padding: 8px 6px; border-radius: 3px; text-align: left; width: 100%; } .sp-hit { display: grid; grid-template-columns: 44px 1fr auto; gap: 14px; align-items: center; padding: 8px 6px; border-radius: 3px; text-align: left; width: 100%; }
.searchpop__hit:hover { background: var(--paper-2); } .sp-hit:hover { background: var(--paper-2); }
.searchpop__hit img { width: 44px; height: 66px; object-fit: cover; background: var(--paper-2); } .sp-hit img { width: 44px; height: 66px; object-fit: cover; background: var(--paper-2); }
.searchpop__hit .t { font: 400 18px/1.1 var(--display); } .sp-hit .t { font: 400 18px/1.1 var(--display); }
.searchpop__hit .s { font: 400 12px/1.3 var(--sans); color: var(--ink-soft); margin-top: 3px; } .sp-hit .s { font: 400 12px/1.3 var(--sans); color: var(--ink-soft); margin-top: 3px; }
.searchpop__hit .n { font: 500 10px/1 var(--mono); color: var(--ink-soft); letter-spacing: .1em; } .sp-hit .n { font: 500 10px/1 var(--mono); color: var(--ink-soft); letter-spacing: .1em; }
/* ── lightbox ── */ /* ── lightbox ── */
.lb { .lb {
@@ -267,68 +133,50 @@ mark.hl { background: color-mix(in oklab, var(--roux) 22%, transparent); color:
@media (max-width: 920px) { @media (max-width: 920px) {
.lb { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto auto; grid-template-areas: "topbar" "stage" "meta" "thumbs"; } .lb { grid-template-columns: 1fr; grid-template-rows: auto 1fr auto auto; grid-template-areas: "topbar" "stage" "meta" "thumbs"; }
} }
.lb__topbar { grid-area: topbar; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 22px; border-bottom: 1px solid rgba(236,231,221,.08); } .lb-topbar { grid-area: topbar; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 22px; border-bottom: 1px solid rgba(236,231,221,.08); }
.lb__brand { display: flex; align-items: center; gap: 14px; font: 500 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: #c8c0b1; } .lb-brand { display: flex; align-items: center; gap: 14px; font: 500 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; color: #c8c0b1; }
.lb__brand svg { height: 18px; } .lb-brand svg { height: 18px; }
.lb__index { font: 500 11px/1 var(--mono); letter-spacing: .14em; color: #c8c0b1; font-variant-numeric: tabular-nums; } .lb-index { font: 500 11px/1 var(--mono); letter-spacing: .14em; color: #c8c0b1; font-variant-numeric: tabular-nums; }
.lb__index b { color: #ece7dd; } .lb-index b { color: #ece7dd; }
.lb__close { width: 36px; height: 36px; border: 1px solid rgba(236,231,221,.18); border-radius: 50%; display: grid; place-items: center; color: #ece7dd; } .lb-close { width: 36px; height: 36px; border: 1px solid rgba(236,231,221,.18); border-radius: 50%; display: grid; place-items: center; color: #ece7dd; }
.lb__close:hover { background: rgba(236,231,221,.08); } .lb-close:hover { background: rgba(236,231,221,.08); }
.lb__stage { grid-area: stage; position: relative; overflow: hidden; display: grid; place-items: center; padding: 24px; min-height: 0; } .lb-stage { grid-area: stage; position: relative; overflow: hidden; display: grid; place-items: center; padding: 24px; min-height: 0; }
.lb__track { position: absolute; inset: 0; display: flex; transition: transform .55s cubic-bezier(.4,.0,.2,1); will-change: transform; } .lb-track { position: absolute; inset: 0; display: flex; transition: transform .55s cubic-bezier(.4,.0,.2,1); will-change: transform; }
.lb__slide { flex: 0 0 100%; height: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; min-width: 0; } .lb-slide { flex: 0 0 100%; height: 100%; display: flex; align-items: center; justify-content: center; padding: 24px; min-width: 0; }
.lb__frame { position: relative; aspect-ratio: 2/3; height: 100%; max-width: 100%; background: #1a1411; box-shadow: 0 30px 80px -20px rgba(0,0,0,.6), 0 0 0 1px rgba(236,231,221,.04); } .lb-frame { position: relative; aspect-ratio: 2/3; height: 100%; max-width: 100%; background: #1a1411; box-shadow: 0 30px 80px -20px rgba(0,0,0,.6), 0 0 0 1px rgba(236,231,221,.04); }
.lb__img { width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; } .lb-img { width: 100%; height: 100%; object-fit: cover; object-position: center; display: block; }
.lb__nav { position: absolute; top: 50%; transform: translateY(-50%); width: 56px; height: 56px; border-radius: 50%; background: rgba(236,231,221,.06); border: 1px solid rgba(236,231,221,.16); backdrop-filter: blur(8px); display: grid; place-items: center; color: #ece7dd; z-index: 5; transition: background .2s; } .lb-nav { position: absolute; top: 50%; transform: translateY(-50%); width: 56px; height: 56px; border-radius: 50%; background: rgba(236,231,221,.06); border: 1px solid rgba(236,231,221,.16); backdrop-filter: blur(8px); display: grid; place-items: center; color: #ece7dd; z-index: 5; transition: background .2s; }
.lb__nav:hover { background: rgba(236,231,221,.14); } .lb-nav:hover { background: rgba(236,231,221,.14); }
.lb__nav--prev { left: 18px; } .lb-nav-prev { left: 18px; }
.lb__nav--next { right: 18px; } .lb-nav-next { right: 18px; }
.lb__meta { grid-area: meta; padding: 28px 28px 22px; border-left: 1px solid rgba(236,231,221,.08); display: flex; flex-direction: column; gap: 18px; overflow: auto; } .lb-meta { grid-area: meta; padding: 28px 28px 22px; border-left: 1px solid rgba(236,231,221,.08); display: flex; flex-direction: column; gap: 18px; overflow: auto; }
@media (max-width: 920px) { .lb__meta { border-left: 0; border-top: 1px solid rgba(236,231,221,.08); padding: 18px 22px; max-height: 38vh; } } @media (max-width: 920px) { .lb-meta { border-left: 0; border-top: 1px solid rgba(236,231,221,.08); padding: 18px 22px; max-height: 38vh; } }
.lb__cat { font: 500 11px/1 var(--sans); letter-spacing: .22em; text-transform: uppercase; color: var(--roux); } .lb-cat { font: 500 11px/1 var(--sans); letter-spacing: .22em; text-transform: uppercase; color: var(--roux); }
.lb__cat::after { content: ""; display: block; height: 1px; width: 30px; background: var(--roux); margin-top: 8px; } .lb-cat::after { content: ""; display: block; height: 1px; width: 30px; background: var(--roux); margin-top: 8px; }
.lb__title { margin: 0; font: 400 clamp(32px, 3vw, 44px)/1 var(--display); letter-spacing: -0.005em; color: #f5f0e6; } .lb-title { margin: 0; font: 400 clamp(32px,3vw,44px)/1 var(--display); letter-spacing: -0.005em; color: #f5f0e6; }
.lb__title em { font-family: var(--serif); font-style: italic; color: #d99e8e; font-weight: 300; } .lb-title em { font-family: var(--serif); font-style: italic; color: #d99e8e; font-weight: 300; }
.lb__desc { font: 400 16px/1.55 var(--serif); font-style: italic; color: #c8c0b1; } .lb-desc { font: 400 16px/1.55 var(--serif); font-style: italic; color: #c8c0b1; }
.lb__factgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 22px; border-top: 1px solid rgba(236,231,221,.08); border-bottom: 1px solid rgba(236,231,221,.08); padding: 16px 0; } .lb-facts { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 22px; border-top: 1px solid rgba(236,231,221,.08); border-bottom: 1px solid rgba(236,231,221,.08); padding: 16px 0; }
.lb__fact dt { font: 500 10px/1 var(--sans); letter-spacing: .18em; text-transform: uppercase; color: #9b9384; margin-bottom: 5px; } .lb-fact dt { font: 500 10px/1 var(--sans); letter-spacing: .18em; text-transform: uppercase; color: #9b9384; margin-bottom: 5px; }
.lb__fact dd { margin: 0; font: 400 14px/1.3 var(--serif); color: #ece7dd; } .lb-fact dd { margin: 0; font: 400 14px/1.3 var(--serif); color: #ece7dd; }
.lb__tags { display: flex; flex-wrap: wrap; gap: 6px; } .lb-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.lb__tag { padding: 5px 10px; border: 1px solid rgba(236,231,221,.18); border-radius: 999px; font: 500 10px/1 var(--sans); letter-spacing: .1em; text-transform: lowercase; color: #ece7dd; } .lb-tag { padding: 5px 10px; border: 1px solid rgba(236,231,221,.18); border-radius: 999px; font: 500 10px/1 var(--sans); letter-spacing: .1em; text-transform: lowercase; color: #ece7dd; }
.lb__tag:hover { background: rgba(236,231,221,.08); } .lb-tag:hover { background: rgba(236,231,221,.08); }
.lb__share { margin-top: auto; display: flex; gap: 8px; } .lb-share { margin-top: auto; display: flex; gap: 8px; }
.lb__sh { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 11px 14px; border: 1px solid rgba(236,231,221,.2); font: 500 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: #ece7dd; white-space: nowrap; transition: background .2s, color .2s, border-color .2s; } .lb-sh { display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 11px 14px; border: 1px solid rgba(236,231,221,.2); font: 500 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: #ece7dd; white-space: nowrap; transition: background .2s, color .2s, border-color .2s; }
.lb__sh--primary { flex: 1; min-width: 0; } .lb-sh-primary { flex: 1; min-width: 0; }
.lb__sh:not(.lb__sh--primary) { width: 44px; padding: 11px 0; flex: 0 0 auto; } .lb-sh:not(.lb-sh-primary) { width: 44px; padding: 11px 0; flex: 0 0 auto; }
.lb__sh:hover { background: #ece7dd; color: #0c0907; border-color: #ece7dd; } .lb-sh:hover { background: #ece7dd; color: #0c0907; border-color: #ece7dd; }
.lb__sh.is-ok { background: rgba(236,231,221,.08); color: #ece7dd; border-color: rgba(236,231,221,.32); } .lb-sh.is-ok { background: rgba(236,231,221,.08); color: #ece7dd; border-color: rgba(236,231,221,.32); }
.lb__thumbs { grid-area: thumbs; display: flex; gap: 6px; padding: 12px 22px 16px; overflow-x: auto; border-top: 1px solid rgba(236,231,221,.08); scrollbar-width: thin; scrollbar-color: rgba(236,231,221,.16) transparent; } .lb-thumbs { grid-area: thumbs; display: flex; gap: 6px; padding: 12px 22px 16px; overflow-x: auto; border-top: 1px solid rgba(236,231,221,.08); scrollbar-width: thin; scrollbar-color: rgba(236,231,221,.16) transparent; }
.lb__thumb { flex: 0 0 auto; width: 42px; aspect-ratio: 2/3; background: #1a1411; border: 1px solid transparent; opacity: .45; transition: opacity .2s, border-color .2s; position: relative; } .lb-thumb { flex: 0 0 auto; width: 42px; aspect-ratio: 2/3; background: #1a1411; border: 1px solid transparent; opacity: .45; transition: opacity .2s, border-color .2s; position: relative; }
.lb__thumb:hover { opacity: .8; } .lb-thumb:hover { opacity: .8; }
.lb__thumb img { width: 100%; height: 100%; object-fit: cover; } .lb-thumb img { width: 100%; height: 100%; object-fit: cover; }
.lb__thumb[aria-current="true"] { opacity: 1; border-color: var(--roux); } .lb-thumb[aria-current="true"] { opacity: 1; border-color: var(--roux); }
.lb__thumb[aria-current="true"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: -10px; height: 2px; background: var(--roux); } .lb-thumb[aria-current="true"]::after { content: ""; position: absolute; left: 0; right: 0; bottom: -10px; height: 2px; background: var(--roux); }
/* ── footer ── */
.foot { border-top: 1px solid var(--rule); padding: 36px var(--pad) 56px; display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; gap: 40px; font: 400 12px/1.5 var(--sans); color: var(--ink-soft); }
@media (max-width: 820px) { .foot { grid-template-columns: 1fr 1fr; } }
.foot h4 { font: 500 10px/1 var(--sans); letter-spacing: .22em; text-transform: uppercase; color: var(--ink); margin: 0 0 14px; }
.foot a:hover { color: var(--ink); }
.foot__roux { font: 400 clamp(60px, 8vw, 110px)/0.92 var(--display); color: var(--ink); margin: 0 0 14px; }
.foot__roux em { font-family: var(--serif); font-style: italic; color: var(--roux); font-weight: 300; }
/* ── ribbon ── */ /* ── ribbon ── */
.ribbon { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; background: var(--ink); color: var(--paper); font: 500 11px/1 var(--sans); letter-spacing: .2em; text-transform: uppercase; padding: 10px var(--pad); display: flex; align-items: center; justify-content: space-between; gap: 24px; transform: translateY(0); transition: transform .4s; }
.ribbon.hidden { transform: translateY(100%); } .ribbon.hidden { transform: translateY(100%); }
.ribbon a { color: var(--roux); font-weight: 600; }
.ribbon button { color: var(--paper); opacity: .7; }
.ribbon button:hover { opacity: 1; }
/* ── animations ── */
@keyframes rouxIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
.card { animation: rouxIn .6s cubic-bezier(.2,.7,.2,1) both; animation-delay: var(--d, 0ms); }
.issue-card { opacity: 0; transform: translateY(8px); animation: rouxIn .65s cubic-bezier(.2,.7,.2,1) forwards; animation-delay: var(--d, 0ms); }
@supports (animation-timeline: view()) { .issue-card { opacity: 1; transform: none; animation: none; } }
/* ── view transitions ── */ /* ── view transitions ── */
::view-transition-old(root) { animation: 150ms ease-out roux-fade-out; } ::view-transition-old(root) { animation: 150ms ease-out roux-fade-out; }
+35 -37
View File
@@ -129,16 +129,16 @@
const cats = allCats(); const cats = allCats();
const tags = allTags(); const tags = allTags();
searchPop.innerHTML = ` searchPop.innerHTML = `
<div class="searchpop__section"> <div class="sp-section">
<div class="searchpop__label">Categories <small>${cats.length}</small></div> <div class="sp-label">Categories <small>${cats.length}</small></div>
<div class="searchpop__chips"> <div class="sp-chips">
${cats.map(c => `<button class="searchpop__chip" data-jump="/categories/${encodeURIComponent(c.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> ${esc(c)}</button>`).join('')} ${cats.map(c => `<button class="sp-chip" data-jump="/categories/${encodeURIComponent(c.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> ${esc(c)}</button>`).join('')}
</div> </div>
</div> </div>
<div class="searchpop__section"> <div class="sp-section">
<div class="searchpop__label">Popular tags <small>${tags.length}</small></div> <div class="sp-label">Popular tags <small>${tags.length}</small></div>
<div class="searchpop__chips"> <div class="sp-chips">
${tags.map(t => `<button class="searchpop__chip" data-jump="/tags/${encodeURIComponent(t.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> # ${esc(t)}</button>`).join('')} ${tags.map(t => `<button class="sp-chip" data-jump="/tags/${encodeURIComponent(t.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> # ${esc(t)}</button>`).join('')}
</div> </div>
</div>`; </div>`;
searchPop.dataset.open = 'true'; searchPop.dataset.open = 'true';
@@ -147,17 +147,17 @@
const hits = INDEX(q) || []; const hits = INDEX(q) || [];
const terms = q.toLowerCase().split(/\s+/).filter(t => t.length > 1); const terms = q.toLowerCase().split(/\s+/).filter(t => t.length > 1);
if (!hits.length) { if (!hits.length) {
searchPop.innerHTML = `<div class="searchpop__section"><p style="color:var(--ink-soft);font-size:13px;font-style:italic">No plates match — try <em>gothic</em>, <em>warrior</em>, or <em>neon</em>.</p></div>`; searchPop.innerHTML = `<div class="sp-section"><p style="color:var(--ink-soft);font-size:13px;font-style:italic">No plates match — try <em>gothic</em>, <em>warrior</em>, or <em>neon</em>.</p></div>`;
searchPop.dataset.open = 'true'; searchPop.dataset.open = 'true';
return; return;
} }
const shown = hits.slice(0, 6); const shown = hits.slice(0, 6);
searchPop.innerHTML = ` searchPop.innerHTML = `
<div class="searchpop__section"> <div class="sp-section">
<div class="searchpop__label">Plates <small>${hits.length}</small></div> <div class="sp-label">Plates <small>${hits.length}</small></div>
<div class="searchpop__hits"> <div class="sp-hits">
${shown.map(p => ` ${shown.map(p => `
<button class="searchpop__hit" data-jump="${esc(p.url)}"> <button class="sp-hit" data-jump="${esc(p.url)}">
${p.thumb ? `<img src="${esc(p.thumb)}" alt="" loading="lazy" />` : '<div style="width:44px;height:66px;background:var(--paper-2)"></div>'} ${p.thumb ? `<img src="${esc(p.thumb)}" alt="" loading="lazy" />` : '<div style="width:44px;height:66px;background:var(--paper-2)"></div>'}
<div> <div>
<div class="t">${highlight(p.title, terms)}</div> <div class="t">${highlight(p.title, terms)}</div>
@@ -241,14 +241,13 @@
if (!lbTrack) return; if (!lbTrack) return;
lbTrack.innerHTML = lbList.map((p, i) => { lbTrack.innerHTML = lbList.map((p, i) => {
const imgSrc = p.card || p.thumb || ''; const imgSrc = p.card || p.thumb || '';
return `<div class="lb__slide" data-i="${i}"> return `<div class="lb-slide" data-i="${i}">
<div class="lb__frame"> <div class="lb-frame">
<img class="lb__img" src="${esc(imgSrc)}" alt="${esc(p.title)}" loading="lazy" /> <img class="lb-img" src="${esc(imgSrc)}" alt="${esc(p.title)}" loading="lazy" />
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
lbBuilt = true; lbBuilt = true;
// Preload neighbors
preloadNeighbors(lbIdx); preloadNeighbors(lbIdx);
} }
@@ -271,7 +270,7 @@
[-1, 0, 1, 2].forEach(d => { [-1, 0, 1, 2].forEach(d => {
const ni = idx + d; const ni = idx + d;
if (ni < 0 || ni >= lbList.length) return; if (ni < 0 || ni >= lbList.length) return;
const slide = lbTrack.querySelector(`.lb__slide[data-i="${ni}"]`); const slide = lbTrack.querySelector(`.lb-slide[data-i="${ni}"]`);
if (!slide) return; if (!slide) return;
const img = slide.querySelector('img'); const img = slide.querySelector('img');
if (img && !img.src) img.src = lbList[ni].card || lbList[ni].thumb || ''; if (img && !img.src) img.src = lbList[ni].card || lbList[ni].thumb || '';
@@ -283,22 +282,22 @@
const cats = (p.categories || []).join(', '); const cats = (p.categories || []).join(', ');
const tags = (p.tags || []).slice(0, 8); const tags = (p.tags || []).slice(0, 8);
lbMeta.innerHTML = ` lbMeta.innerHTML = `
<div class="lb__cat">${esc(cats)}</div> <div class="lb-cat">${esc(cats)}</div>
<h2 class="lb__title">${esc(p.title)}</h2> <h2 class="lb-title">${esc(p.title)}</h2>
<p class="lb__desc">${esc(p.description)}</p> <p class="lb-desc">${esc(p.description)}</p>
<dl class="lb__factgrid"> <dl class="lb-facts">
<div class="lb__fact"><dt>Plate</dt><dd>№ ${esc(p.id)}</dd></div> <div class="lb-fact"><dt>Plate</dt><dd>№ ${esc(p.id)}</dd></div>
<div class="lb__fact"><dt>Issue</dt><dd><a href="/issues/${esc(p.issue || '01')}/" style="color:inherit;border-bottom:1px solid currentColor">№ ${esc(p.issue || '01')}</a></dd></div> <div class="lb-fact"><dt>Issue</dt><dd><a href="/issues/${esc(p.issue || '01')}/" style="color:inherit;border-bottom:1px solid currentColor">№ ${esc(p.issue || '01')}</a></dd></div>
<div class="lb__fact"><dt>Category</dt><dd>${esc((p.categories||['—'])[0])}</dd></div> <div class="lb-fact"><dt>Category</dt><dd>${esc((p.categories||['—'])[0])}</dd></div>
</dl> </dl>
<div class="lb__tags">${tags.map(t => { <div class="lb-tags">${tags.map(t => {
const slug = t.toLowerCase().replace(/[\s,]+/g, '-').replace(/[^a-z0-9-]/g, ''); const slug = t.toLowerCase().replace(/[\s,]+/g, '-').replace(/[^a-z0-9-]/g, '');
return `<a class="lb__tag" href="/tags/${slug}/"># ${esc(t)}</a>`; return `<a class="lb-tag" href="/tags/${slug}/"># ${esc(t)}</a>`;
}).join('')}</div> }).join('')}</div>
<div class="lb__share"> <div class="lb-share">
<button class="lb__sh lb__sh--primary" id="lbCopy" data-url="${esc(p.url)}"> <button class="lb-sh lb-sh-primary" id="lbCopy" data-url="${esc(p.url)}">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 1 0-7.07-7.07l-1 1"/><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 1 0 7.07 7.07l1-1"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 1 0-7.07-7.07l-1 1"/><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 1 0 7.07 7.07l1-1"/></svg>
<span class="lb__sh-l">Copy link</span> <span class="lb-sh-l">Copy link</span>
</button> </button>
</div>`; </div>`;
@@ -306,8 +305,8 @@
const url = new URL(this.dataset.url, location.origin).href; const url = new URL(this.dataset.url, location.origin).href;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
this.classList.add('is-ok'); this.classList.add('is-ok');
this.querySelector('.lb__sh-l').textContent = 'Copied!'; this.querySelector('.lb-sh-l').textContent = 'Copied!';
setTimeout(() => { this.classList.remove('is-ok'); this.querySelector('.lb__sh-l').textContent = 'Copy link'; }, 2000); setTimeout(() => { this.classList.remove('is-ok'); this.querySelector('.lb-sh-l').textContent = 'Copy link'; }, 2000);
}); });
}); });
} }
@@ -315,22 +314,22 @@
function lbBuildThumbs() { function lbBuildThumbs() {
if (!lbThumbs) return; if (!lbThumbs) return;
lbThumbs.innerHTML = lbList.map((p, i) => lbThumbs.innerHTML = lbList.map((p, i) =>
`<button class="lb__thumb" data-i="${i}" aria-current="${i === lbIdx}" aria-label="${esc(p.title)}"> `<button class="lb-thumb" data-i="${i}" aria-current="${i === lbIdx}" aria-label="${esc(p.title)}">
<img src="${esc(p.thumb || p.card || '')}" alt="" loading="lazy" /> <img src="${esc(p.thumb || p.card || '')}" alt="" loading="lazy" />
</button>` </button>`
).join(''); ).join('');
lbThumbs.addEventListener('click', e => { lbThumbs.addEventListener('click', e => {
const btn = e.target.closest('.lb__thumb'); const btn = e.target.closest('.lb-thumb');
if (btn) goToSlide(parseInt(btn.dataset.i)); if (btn) goToSlide(parseInt(btn.dataset.i));
}); });
} }
function syncThumbs() { function syncThumbs() {
if (!lbThumbs) return; if (!lbThumbs) return;
lbThumbs.querySelectorAll('.lb__thumb').forEach((b, i) => { lbThumbs.querySelectorAll('.lb-thumb').forEach((b, i) => {
b.setAttribute('aria-current', i === lbIdx ? 'true' : 'false'); b.setAttribute('aria-current', i === lbIdx ? 'true' : 'false');
}); });
const active = lbThumbs.querySelector('.lb__thumb[aria-current="true"]'); const active = lbThumbs.querySelector('.lb-thumb[aria-current="true"]');
if (active) active.scrollIntoView({ inline: 'center', behavior: 'smooth' }); if (active) active.scrollIntoView({ inline: 'center', behavior: 'smooth' });
} }
@@ -432,7 +431,6 @@
syncTabs(); syncTabs();
// If new page has a slug to open // If new page has a slug to open
const script = doc.querySelector('script[data-open-slug]');
const openSlug = window.__ROUX_OPEN_SLUG = doc.querySelector('[data-open-slug]')?.dataset.openSlug || null; const openSlug = window.__ROUX_OPEN_SLUG = doc.querySelector('[data-open-slug]')?.dataset.openSlug || null;
if (openSlug) { if (openSlug) {
const opened = POSTS.find(p => p.slug === openSlug); const opened = POSTS.find(p => p.slug === openSlug);
+211 -58
View File
@@ -40,69 +40,222 @@
"title" "title"
], ],
"classes": [ "classes": [
"[&::-webkit-scrollbar]:hidden",
"[&_b]:font-semibold",
"[&_b]:mr-1",
"[&_b]:text-ink",
"[-webkit-box-orient:vertical]",
"[-webkit-line-clamp:2]",
"[background:color-mix(in_oklab,var(--paper)_86%,transparent)]",
"[background:color-mix(in_oklab,var(--paper)_95%,transparent)]",
"[display:-webkit-box]",
"[filter:saturate(.92)_contrast(1.02)]",
"[grid-template-columns:1fr_auto_1fr]",
"[grid-template-columns:2fr_1fr_1fr_1fr]",
"[grid-template-columns:repeat(auto-fill,minmax(220px,1fr))]",
"[grid-template-columns:repeat(auto-fill,minmax(280px,1fr))]",
"[scrollbar-width:none]",
"[transition-duration:.9s,.6s]",
"[transition-timing-function:cubic-bezier(.2,.7,.1,1),ease]",
"absolute",
"after:absolute",
"after:content-['']",
"after:inset-0",
"after:pointer-events-none",
"after:shadow-[inset_0_0_0_1px_rgba(22,17,13,.04)]",
"align-top",
"aspect-[2/3]",
"backdrop-blur-[14px]",
"bg-[var(--paper)]",
"bg-ink",
"bg-paper-2",
"bg-transparent",
"block",
"border",
"border-0",
"border-[var(--rule)]",
"border-[var(--rule-2)]",
"border-b",
"border-current",
"border-current/30",
"border-r",
"border-t",
"border-transparent",
"bottom-0",
"bottom-[10px]",
"card", "card",
"card__cat", "card-img",
"card__desc", "cursor-pointer",
"card__frame", "cursor-text",
"card__img", "duration-200",
"card__meta", "duration-300",
"card__num", "duration-700",
"card__sub", "fixed",
"card__title", "flex",
"dot", "flex-1",
"foot", "flex-col",
"foot__roux", "font-display",
"font-light",
"font-medium",
"font-mono",
"font-normal",
"font-sans",
"font-serif",
"gap-1",
"gap-10",
"gap-3",
"gap-4",
"gap-[18px]",
"gap-[clamp(12px,2vw,24px)]",
"gap-[var(--gap)]",
"grid", "grid",
"hero", "group",
"hero__eyebrow", "group-hover:scale-[1.02]",
"hero__lede", "h-6",
"hero__title", "h-[30px]",
"issue-card", "h-full",
"issue-card--forthcoming", "hover:opacity-100",
"issue-card__blurb", "hover:text-ink",
"issue-card__cover", "inset-0",
"issue-card__cta", "inset-x-0",
"issue-card__cta--muted", "italic",
"issue-card__foot", "items-center",
"issue-card__forth", "items-stretch",
"issue-card__meta", "justify-between",
"issue-card__num", "justify-center",
"issue-card__title", "justify-end",
"issues-grid", "justify-items-center",
"justify-self-center",
"lb", "lb",
"lb__brand", "lb-brand",
"lb__close", "lb-close",
"lb__index", "lb-index",
"lb__meta", "lb-meta",
"lb__nav", "lb-nav",
"lb__nav--next", "lb-nav-next",
"lb__nav--prev", "lb-nav-prev",
"lb__stage", "lb-stage",
"lb__thumbs", "lb-thumbs",
"lb__topbar", "lb-topbar",
"lb__track", "lb-track",
"logo", "leading-[0.92]",
"logo__mark", "leading-[0.94]",
"logo__tag", "leading-[1.05]",
"logo__word", "leading-[1.1]",
"masthead", "leading-[1.4]",
"masthead__date", "leading-[1.5]",
"masthead__inner", "leading-[1.65]",
"masthead__left", "leading-none",
"masthead__logo", "left-[10px]",
"masthead__right", "m-0",
"mh-city", "max-[1100px]:hidden",
"mh-issue", "max-[460px]:[grid-template-columns:1fr]",
"mh-link", "max-[460px]:hidden",
"mh-pub", "max-[620px]:gap-[10px]",
"mh-sep", "max-[620px]:h-[22px]",
"max-[620px]:py-3",
"max-[620px]:text-[10px]",
"max-[620px]:text-[22px]",
"max-[620px]:text-[7.5px]",
"max-[620px]:tracking-[.28em]",
"max-[620px]:w-[22px]",
"max-[720px]:hidden",
"max-[720px]:text-[15px]",
"max-[820px]:[grid-template-columns:1fr_1fr]",
"max-[820px]:hidden",
"max-w-[55ch]",
"mb-2",
"mb-4",
"mb-5",
"mb-[14px]",
"min-w-0",
"mix-blend-difference",
"ml-1",
"ml-2",
"mt-0.5",
"mt-[14px]",
"mt-auto",
"nav-link",
"object-cover",
"opacity-50",
"opacity-60",
"outline-none",
"overflow-hidden",
"overflow-x-auto",
"pb-14",
"pb-[clamp(28px,3.5vw,48px)]",
"place-items-center",
"placeholder:opacity-90",
"placeholder:text-ink-soft",
"pt-4",
"pt-9",
"pt-[clamp(36px,5vw,64px)]",
"px-[14px]",
"px-[6px]",
"px-[7px]",
"px-[var(--pad)]",
"py-1.5",
"py-2",
"py-3",
"py-[10px]",
"py-[14px]",
"py-[3px]",
"py-[5px]",
"py-[var(--gap)]",
"relative",
"ribbon", "ribbon",
"right-[10px]",
"rounded-[1px]",
"rounded-[3px]",
"rounded-full",
"saturate-[1.05]",
"searchpop", "searchpop",
"subhead", "shrink-0",
"subhead__count", "sticky",
"subhead__kbd", "tabs",
"subhead__search", "tabular-nums",
"tabs" "text-[10px]",
"text-[11px]",
"text-[12px]",
"text-[13px]",
"text-[16px]",
"text-[30px]",
"text-[8.5px]",
"text-[clamp(13px,1vw,16px)]",
"text-[clamp(20px,1.6vw,26px)]",
"text-[clamp(22px,2vw,30px)]",
"text-[clamp(48px,7vw,96px)]",
"text-[clamp(60px,8vw,110px)]",
"text-center",
"text-ink",
"text-ink-2",
"text-ink-soft",
"text-paper",
"text-roux",
"top-0",
"top-[10px]",
"tracking-[.005em]",
"tracking-[.045em]",
"tracking-[.04em]",
"tracking-[.12em]",
"tracking-[.14em]",
"tracking-[.16em]",
"tracking-[.18em]",
"tracking-[.1em]",
"tracking-[.22em]",
"tracking-[.32em]",
"tracking-normal",
"transition-[color,border-color,background]",
"transition-[transform,filter]",
"transition-opacity",
"transition-transform",
"uppercase",
"w-6",
"w-[30px]",
"w-full",
"whitespace-nowrap",
"z-50",
"z-[100]"
], ],
"ids": [ "ids": [
"content", "content",
+18 -43
View File
@@ -1,60 +1,35 @@
{{ define "main" }} {{ define "main" }}
{{/* Category / tag taxonomy list pages */}} {{/* Category / tag taxonomy list pages */}}
<section class="hero" id="hero"> <section id="hero" class="px-[var(--pad)] pt-[clamp(36px,5vw,64px)] pb-[clamp(28px,3.5vw,48px)] border-b border-[var(--rule)]">
{{- if eq .Kind "taxonomy" }} {{- if eq .Kind "taxonomy" }}
<div class="hero__eyebrow">{{ .Type | humanize }}</div> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">{{ .Type | humanize }}</div>
<h1 class="hero__title"><em>{{ .Title }}</em></h1> <h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4"><em>{{ .Title }}</em></h1>
<p class="hero__lede"> <p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">
{{ len .Pages }} {{ if eq (len .Pages) 1 }}plate{{ else }}plates{{ end }} in this section. {{ len .Pages }} {{ if eq (len .Pages) 1 }}plate{{ else }}plates{{ end }} in this section.
<a href="/" style="border-bottom:1px solid currentColor">Return to archive →</a> <a href="/" class="border-b border-current">Return to archive →</a>
</p> </p>
{{- else if eq .Kind "term" }} {{- else if eq .Kind "term" }}
<div class="hero__eyebrow">{{ .Type | humanize | singularize }}</div> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">{{ .Type | humanize | singularize }}</div>
<h1 class="hero__title"><em>{{ .Title }}</em></h1> <h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4"><em>{{ .Title }}</em></h1>
<p class="hero__lede"> <p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">
{{ len .Pages }} {{ if eq (len .Pages) 1 }}plate{{ else }}plates{{ end }} tagged <em>{{ .Title }}</em>. {{ len .Pages }} {{ if eq (len .Pages) 1 }}plate{{ else }}plates{{ end }} tagged <em>{{ .Title }}</em>.
<a href="/" style="border-bottom:1px solid currentColor">Return to archive →</a> <a href="/" class="border-b border-current">Return to archive →</a>
</p> </p>
{{- else }} {{- else }}
<div class="hero__eyebrow">The Archive</div> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">The Archive</div>
<h1 class="hero__title">All <em>plates</em></h1> <h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4">All <em>plates</em></h1>
<p class="hero__lede"><a href="/" style="border-bottom:1px solid currentColor">Return to home →</a></p> <p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">
<a href="/" class="border-b border-current">Return to home →</a>
</p>
{{- end }} {{- end }}
</section> </section>
<section class="grid" id="grid" data-density="default" <section id="grid" data-density="default"
data-filter-type="{{ .Kind }}" data-filter-type="{{ .Kind }}"
data-filter-value="{{ .Title }}"> data-filter-value="{{ .Title }}"
class="grid [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))] gap-[var(--gap)] px-[var(--pad)] py-[var(--gap)]">
{{- range $i, $p := .Pages }} {{- range $i, $p := .Pages }}
{{- $img := $p.Resources.GetMatch "*.png" }} {{- partial "card.html" (dict "Page" $p "Index" $i) }}
<a class="card"
href="{{ $p.RelPermalink }}"
style="--d:{{ mul (math.Min $i 18) 24 }}ms"
data-id="{{ $p.Params.plate }}"
data-slug="{{ $p.Params.slug }}">
<div class="card__frame">
{{- if $img }}
{{- $w := $img.Resize "600x900 webp" }}
<picture>
<source srcset="{{ $w.RelPermalink }}" type="image/webp" />
<img class="card__img"
loading="{{ if lt $i 8 }}eager{{ else }}lazy{{ end }}"
src="{{ $w.RelPermalink }}"
alt="{{ $p.Title }}" />
</picture>
{{- end }}
<span class="card__num">PLATE №{{ $p.Params.plate }}</span>
<span class="card__cat">{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
<span class="card__issue">№ {{ index ($p.Params.issues | default (slice "01")) 0 }}</span>
</div>
<div class="card__meta">
<h2 class="card__title">{{ $p.Title }}</h2>
<div class="card__sub">
<span>{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
</div>
<p class="card__desc">{{ $p.Params.description }}</p>
</div>
</a>
{{- end }} {{- end }}
</section> </section>
{{ end }} {{ end }}
+8 -35
View File
@@ -1,50 +1,23 @@
{{ define "main" }} {{ define "main" }}
{{- $issue := index (.Params.issues | default (slice "01")) 0 }} {{- $issue := index (.Params.issues | default (slice "01")) 0 }}
{{- $issueURL := printf "/issues/%s/" $issue }} {{- $issueURL := printf "/issues/%s/" $issue }}
<section class="hero" id="hero"> <section id="hero" class="px-[var(--pad)] pt-[clamp(36px,5vw,64px)] pb-[clamp(28px,3.5vw,48px)] border-b border-[var(--rule)]">
<div class="hero__eyebrow"> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">
№ {{ $issue }} · {{ index (.Params.categories | default (slice "Plate")) 0 }} № {{ $issue }} · {{ index (.Params.categories | default (slice "Plate")) 0 }}
</div> </div>
<h1 class="hero__title"><em>{{ .Title }}</em></h1> <h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4"><em>{{ .Title }}</em></h1>
<p class="hero__lede"> <p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">
{{ .Params.description }} {{ .Params.description }}
<a href="{{ $issueURL }}" style="border-bottom:1px solid currentColor">Return to issue →</a> <a href="{{ $issueURL }}" class="border-b border-current ml-1">Return to issue →</a>
</p> </p>
</section> </section>
<section class="grid" id="grid" data-density="default"> <section id="grid" data-density="default"
class="grid [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))] gap-[var(--gap)] px-[var(--pad)] py-[var(--gap)]">
{{- $termPage := $.Site.GetPage (printf "/issues/%s" $issue) }} {{- $termPage := $.Site.GetPage (printf "/issues/%s" $issue) }}
{{- $posts := cond (ne $termPage nil) $termPage.Pages .Site.RegularPages }} {{- $posts := cond (ne $termPage nil) $termPage.Pages .Site.RegularPages }}
{{- range $i, $p := $posts }} {{- range $i, $p := $posts }}
{{- $img := $p.Resources.GetMatch "*.png" }} {{- partial "card.html" (dict "Page" $p "Index" $i) }}
<a class="card"
href="{{ $p.RelPermalink }}"
style="--d:{{ mul (math.Min $i 18) 24 }}ms"
data-id="{{ $p.Params.plate }}"
data-slug="{{ $p.Params.slug }}">
<div class="card__frame">
{{- if $img }}
{{- $w := $img.Resize "600x900 webp" }}
<picture>
<source srcset="{{ $w.RelPermalink }}" type="image/webp" />
<img class="card__img"
loading="{{ if lt $i 8 }}eager{{ else }}lazy{{ end }}"
src="{{ $w.RelPermalink }}"
alt="{{ $p.Title }}" />
</picture>
{{- end }}
<span class="card__num">PLATE №{{ $p.Params.plate }}</span>
<span class="card__cat">{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
<span class="card__issue">№ {{ index ($p.Params.issues | default (slice "01")) 0 }}</span>
</div>
<div class="card__meta">
<h2 class="card__title">{{ $p.Title }}</h2>
<div class="card__sub">
<span>{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
</div>
<p class="card__desc">{{ $p.Params.description }}</p>
</div>
</a>
{{- end }} {{- end }}
</section> </section>
+12 -4
View File
@@ -14,10 +14,18 @@
{{- partial "lightbox.html" . }} {{- partial "lightbox.html" . }}
<div class="ribbon" id="ribbon"> <div id="ribbon"
<span>Roux № {{ .Site.Params.issueNumber }} — out now. <a href="/issues/01/">See the plates →</a></span> class="ribbon fixed bottom-0 inset-x-0 z-[100]
<button id="ribbonClose" aria-label="Dismiss"> flex items-center justify-between gap-4
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> px-[var(--pad)] py-3
bg-ink text-paper
font-sans font-medium text-[11px] leading-none tracking-[.16em] uppercase
transition-transform duration-300">
<span>Roux № {{ .Site.Params.issueNumber }} — out now. <a href="/issues/01/" class="border-b border-current ml-1">See the plates →</a></span>
<button id="ribbonClose" aria-label="Dismiss"
class="w-6 h-6 shrink-0 grid place-items-center border border-current/30 rounded-full
opacity-60 hover:opacity-100 transition-opacity duration-200">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button> </button>
</div> </div>
+10 -41
View File
@@ -1,55 +1,24 @@
{{ define "main" }} {{ define "main" }}
<section class="hero" id="hero"> <section id="hero" class="px-[var(--pad)] pt-[clamp(36px,5vw,64px)] pb-[clamp(28px,3.5vw,48px)] border-b border-[var(--rule)]">
<div class="hero__eyebrow"> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">
№ {{ .Site.Params.issueNumber }} · {{ .Site.Params.issueSeason }} · № {{ .Site.Params.issueNumber }} · {{ .Site.Params.issueSeason }} ·
<a href="/issues/" style="border-bottom:1px solid currentColor;margin-left:6px">View all issues</a> <a href="/issues/" class="border-b border-current ml-1">View all issues</a>
</div> </div>
<h1 class="hero__title">An almanac<br/>of <em>fabric, light</em>,<br/>and gesture.</h1> <h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4">
<p class="hero__lede"> An almanac<br/>of <em>fabric, light</em>,<br/>and gesture.
</h1>
<p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">
Roux is a slow-publishing fashion journal — one hundred photographs at a time, Roux is a slow-publishing fashion journal — one hundred photographs at a time,
gathered from <em>ateliers, streets, glasshouses and quiet hotel corridors</em>. gathered from <em>ateliers, streets, glasshouses and quiet hotel corridors</em>.
You are reading <em>{{ .Site.Params.issueName }}</em>, our № {{ .Site.Params.issueNumber }} issue. You are reading <em>{{ .Site.Params.issueName }}</em>, our № {{ .Site.Params.issueNumber }} issue.
</p> </p>
</section> </section>
<section class="grid" id="grid" data-density="default"> <section id="grid" data-density="default"
class="grid [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))] gap-[var(--gap)] px-[var(--pad)] py-[var(--gap)]">
{{- $posts := where .Site.RegularPages "Section" "posts" }} {{- $posts := where .Site.RegularPages "Section" "posts" }}
{{- range $i, $p := $posts }} {{- range $i, $p := $posts }}
{{- $img := $p.Resources.GetMatch "*.png" }} {{- partial "card.html" (dict "Page" $p "Index" $i) }}
{{- $card := "" }}
{{- $full := "" }}
{{- if $img }}
{{- $c := $img.Resize "600x900 webp" }}
{{- $card = $c.RelPermalink }}
{{- end }}
<a class="card"
href="{{ $p.RelPermalink }}"
style="--d:{{ mul (math.Min $i 18) 24 }}ms"
data-id="{{ $p.Params.plate }}"
data-slug="{{ $p.Params.slug }}">
<div class="card__frame">
<picture>
{{- if $img }}
{{- $w := $img.Resize "600x900 webp" }}
<source srcset="{{ $w.RelPermalink }}" type="image/webp" />
<img class="card__img"
loading="{{ if lt $i 8 }}eager{{ else }}lazy{{ end }}"
src="{{ $w.RelPermalink }}"
alt="{{ $p.Title }} — Roux Plate №{{ $p.Params.plate }}" />
{{- end }}
</picture>
<span class="card__num">PLATE №{{ $p.Params.plate }}</span>
<span class="card__cat">{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
<span class="card__issue">№ {{ index ($p.Params.issues | default (slice "01")) 0 }}</span>
</div>
<div class="card__meta">
<h2 class="card__title">{{ $p.Title }}</h2>
<div class="card__sub">
<span>{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
</div>
<p class="card__desc">{{ $p.Params.description }}</p>
</div>
</a>
{{- end }} {{- end }}
</section> </section>
{{ end }} {{ end }}
+8 -35
View File
@@ -1,45 +1,18 @@
{{ define "main" }} {{ define "main" }}
{{- $issueNum := .Params.issueNumber | default (printf "№ %s" .Title) }} {{- $issueNum := .Params.issueNumber | default (printf "№ %s" .Title) }}
<section class="hero" id="hero"> <section id="hero" class="px-[var(--pad)] pt-[clamp(36px,5vw,64px)] pb-[clamp(28px,3.5vw,48px)] border-b border-[var(--rule)]">
<div class="hero__eyebrow"> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">
{{ $issueNum }} · {{ .Params.season }} {{ $issueNum }} · {{ .Params.season }}
· <a href="/issues/" style="border-bottom:1px solid currentColor;margin-left:6px">All issues</a> · <a href="/issues/" class="border-b border-current ml-1">All issues</a>
</div> </div>
<h1 class="hero__title"><em>{{ .Title }}</em></h1> <h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4"><em>{{ .Title }}</em></h1>
<p class="hero__lede">{{ .Params.description }}</p> <p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">{{ .Params.description }}</p>
</section> </section>
<section class="grid" id="grid" data-density="default"> <section id="grid" data-density="default"
class="grid [grid-template-columns:repeat(auto-fill,minmax(220px,1fr))] gap-[var(--gap)] px-[var(--pad)] py-[var(--gap)]">
{{- range $i, $p := .Pages }} {{- range $i, $p := .Pages }}
{{- $img := $p.Resources.GetMatch "*.png" }} {{- partial "card.html" (dict "Page" $p "Index" $i) }}
<a class="card"
href="{{ $p.RelPermalink }}"
style="--d:{{ mul (math.Min $i 18) 24 }}ms"
data-id="{{ $p.Params.plate }}"
data-slug="{{ $p.Params.slug }}">
<div class="card__frame">
{{- if $img }}
{{- $w := $img.Resize "600x900 webp" }}
<picture>
<source srcset="{{ $w.RelPermalink }}" type="image/webp" />
<img class="card__img"
loading="{{ if lt $i 8 }}eager{{ else }}lazy{{ end }}"
src="{{ $w.RelPermalink }}"
alt="{{ $p.Title }}" />
</picture>
{{- end }}
<span class="card__num">PLATE №{{ $p.Params.plate }}</span>
<span class="card__cat">{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
<span class="card__issue">№ {{ index ($p.Params.issues | default (slice "01")) 0 }}</span>
</div>
<div class="card__meta">
<h2 class="card__title">{{ $p.Title }}</h2>
<div class="card__sub">
<span>{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
</div>
<p class="card__desc">{{ $p.Params.description }}</p>
</div>
</a>
{{- end }} {{- end }}
</section> </section>
{{ end }} {{ end }}
+33 -25
View File
@@ -1,32 +1,40 @@
{{ define "main" }} {{ define "main" }}
{{- $issues := .Site.Data.issues }} {{- $issues := .Site.Data.issues }}
<section class="hero" id="hero"> <section id="hero" class="px-[var(--pad)] pt-[clamp(36px,5vw,64px)] pb-[clamp(28px,3.5vw,48px)] border-b border-[var(--rule)]">
<div class="hero__eyebrow">Archive · Every issue</div> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink-soft mb-4">Archive · Every issue</div>
<h1 class="hero__title">The <em>issues</em></h1> <h1 class="font-display font-normal text-[clamp(48px,7vw,96px)] leading-[0.94] m-0 mb-4">The <em>issues</em></h1>
<p class="hero__lede">Roux publishes one hundred photographs at a time, once a season. Each issue is a complete thing.</p> <p class="font-serif italic text-[clamp(13px,1vw,16px)] leading-[1.65] text-ink-2 max-w-[55ch] m-0">
Roux publishes one hundred photographs at a time, once a season. Each issue is a complete thing.
</p>
</section> </section>
<div class="issues-grid"> <div class="grid [grid-template-columns:repeat(auto-fill,minmax(280px,1fr))] gap-[var(--gap)] px-[var(--pad)] py-[var(--gap)]">
{{- range $i, $iss := $issues }} {{- range $i, $iss := $issues }}
{{- $delay := mul $i 80 }} {{- $delay := mul $i 80 }}
{{- $isCurrent := eq $iss.status "current" }} {{- $isCurrent := eq $iss.status "current" }}
{{- $isForth := eq $iss.status "forthcoming" }} {{- $isForth := eq $iss.status "forthcoming" }}
{{- $issueURL := printf "/issues/%s/" $iss.id }} {{- $issueURL := printf "/issues/%s/" $iss.id }}
{{- if $isForth }} {{- if $isForth }}
<div class="issue-card issue-card--forthcoming" style="--d:{{ $delay }}ms"> <div data-issue-card style="--d:{{ $delay }}ms"
class="flex flex-col opacity-60">
{{- else }} {{- else }}
<a href="{{ $issueURL }}" class="issue-card" style="--d:{{ $delay }}ms"> <a href="{{ $issueURL }}" data-issue-card style="--d:{{ $delay }}ms"
class="group flex flex-col">
{{- end }} {{- end }}
<div class="issue-card__cover"> {{/* Cover image */}}
<div class="relative aspect-[2/3] overflow-hidden bg-paper-2 rounded-[1px] mb-5
after:content-[''] after:absolute after:inset-0
after:shadow-[inset_0_0_0_1px_rgba(22,17,13,.04)] after:pointer-events-none">
{{- if $isForth }} {{- if $isForth }}
<div class="issue-card__forth"> <div class="absolute inset-0 flex flex-col items-center justify-center gap-3
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> font-sans font-medium text-[10px] leading-none tracking-[.18em] uppercase text-ink-soft">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
Forthcoming Forthcoming
</div> </div>
{{- else }} {{- else }}
{{- $termPage := $.Site.GetPage (printf "/issues/%s" $iss.id) }} {{- $termPage := $.Site.GetPage (printf "/issues/%s" $iss.id) }}
{{- $firstPost := cond (ne $termPage nil) (index $termPage.Pages 0) nil }} {{- $firstPost := cond (ne $termPage nil) (index $termPage.Pages 0) nil }}
{{- if $firstPost }} {{- if $firstPost }}
{{- $img := $firstPost.Resources.GetMatch "*.png" }} {{- $img := $firstPost.Resources.GetMatch "*.png" }}
@@ -34,25 +42,25 @@
{{- $cover := $img.Resize "480x720 webp" }} {{- $cover := $img.Resize "480x720 webp" }}
<picture> <picture>
<source srcset="{{ $cover.RelPermalink }}" type="image/webp" /> <source srcset="{{ $cover.RelPermalink }}" type="image/webp" />
<img src="{{ $cover.RelPermalink }}" alt="Issue {{ $iss.number }} cover" loading="lazy" /> <img src="{{ $cover.RelPermalink }}" alt="Issue {{ $iss.number }} cover" loading="lazy"
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-[1.02]" />
</picture> </picture>
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- end }} {{- end }}
</div> </div>
<div class="issue-card__meta"> {{/* Metadata */}}
<div class="issue-card__num">{{ $iss.number }}</div> <div class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-roux mb-2">{{ $iss.number }}</div>
<h2 class="issue-card__title">{{ $iss.title }}</h2> <h2 class="font-display font-normal text-[clamp(22px,2vw,30px)] leading-[1.05] m-0 mb-2">{{ $iss.title }}</h2>
<p class="issue-card__blurb">{{ $iss.blurb }}</p> <p class="font-serif italic text-[13px] leading-[1.5] text-ink-soft mb-4 flex-1">{{ $iss.blurb }}</p>
<div class="issue-card__foot"> <div class="flex items-center justify-between font-sans text-[11px] leading-none tracking-[.1em] text-ink-soft mt-auto pt-4 border-t border-[var(--rule-2)]">
<span>{{ $iss.season }}</span> <span>{{ $iss.season }}</span>
{{- if $isCurrent }} {{- if $isCurrent }}
<span class="issue-card__cta">Read the issue →</span> <span class="text-ink">Read the issue →</span>
{{- else if $isForth }} {{- else if $isForth }}
<span class="issue-card__cta issue-card__cta--muted">Subscribe →</span> <span class="opacity-50">Subscribe →</span>
{{- end }} {{- end }}
</div>
</div> </div>
{{- if $isForth }}</div>{{- else }}</a>{{- end }} {{- if $isForth }}</div>{{- else }}</a>{{- end }}
+41
View File
@@ -0,0 +1,41 @@
{{- $p := .Page }}
{{- $i := .Index }}
{{- $img := $p.Resources.GetMatch "*.png" }}
<a class="card block cursor-pointer"
href="{{ $p.RelPermalink }}"
style="--d:{{ mul (math.Min $i 18) 24 }}ms"
data-id="{{ $p.Params.plate }}"
data-slug="{{ $p.Params.slug }}">
<div class="relative aspect-[2/3] overflow-hidden bg-paper-2 rounded-[1px]
after:content-[''] after:absolute after:inset-0
after:shadow-[inset_0_0_0_1px_rgba(22,17,13,.04)] after:pointer-events-none">
{{- if $img }}
{{- $w := $img.Resize "600x900 webp" }}
<picture>
<source srcset="{{ $w.RelPermalink }}" type="image/webp" />
<img class="card-img w-full h-full object-cover
transition-[transform,filter] [transition-duration:.9s,.6s]
[transition-timing-function:cubic-bezier(.2,.7,.1,1),ease]
[filter:saturate(.92)_contrast(1.02)]"
loading="{{ if lt $i 8 }}eager{{ else }}lazy{{ end }}"
src="{{ $w.RelPermalink }}"
alt="{{ $p.Title }}" />
</picture>
{{- end }}
<span class="absolute top-[10px] left-[10px]
font-sans font-medium text-[10px] leading-none tracking-[.18em]
text-paper mix-blend-difference tabular-nums">PLATE №{{ $p.Params.plate }}</span>
<span class="absolute top-[10px] right-[10px]
font-sans font-medium text-[10px] leading-none tracking-[.18em]
uppercase text-paper mix-blend-difference">{{ index ($p.Params.categories | default (slice "")) 0 }}</span>
<span class="absolute bottom-[10px] left-[10px]
font-sans font-medium text-[10px] leading-none tracking-[.18em]
text-paper mix-blend-difference tabular-nums">№ {{ index ($p.Params.issues | default (slice "01")) 0 }}</span>
</div>
<div class="mt-[14px] grid gap-1">
<h2 class="font-display font-normal text-[clamp(20px,1.6vw,26px)] leading-[1.1] tracking-normal m-0">{{ $p.Title }}</h2>
<p class="font-sans text-[12px] leading-[1.4] tracking-[.04em] text-ink-soft">{{ index ($p.Params.categories | default (slice "")) 0 }}</p>
<p class="font-serif text-[13px] leading-[1.5] text-ink-2 italic mt-0.5
overflow-hidden [display:-webkit-box] [-webkit-line-clamp:2] [-webkit-box-orient:vertical]">{{ $p.Params.description }}</p>
</div>
</a>
+25 -17
View File
@@ -1,28 +1,36 @@
<footer class="foot"> <footer class="border-t border-[var(--rule)] px-[var(--pad)] pt-9 pb-14
grid gap-10 font-sans text-[12px] leading-[1.5] text-ink-soft
[grid-template-columns:2fr_1fr_1fr_1fr]
max-[820px]:[grid-template-columns:1fr_1fr]">
<div> <div>
<h4>Roux</h4> <h4 class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink m-0 mb-[14px]">Roux</h4>
<h2 class="foot__roux">R<em>o</em>ux<sup style="font-family:var(--mono);font-size:11px;letter-spacing:.14em;color:var(--ink-soft);vertical-align:top;margin-left:8px;">№{{ .Site.Params.issueNumber }}</sup></h2> <h2 class="font-display font-normal text-[clamp(60px,8vw,110px)] leading-[0.92] text-ink m-0 mb-[14px]">
R<em class="font-serif italic text-roux font-light">o</em>ux<sup class="font-mono text-[11px] tracking-[.14em] text-ink-soft align-top ml-2">№{{ .Site.Params.issueNumber }}</sup>
</h2>
<p>A slow-publishing fashion journal, gathered in Paris. One hundred photographs at a time, printed and unprinted.<br/>roux.pivoine.art</p> <p>A slow-publishing fashion journal, gathered in Paris. One hundred photographs at a time, printed and unprinted.<br/>roux.pivoine.art</p>
</div> </div>
<div> <div>
<h4>Categories</h4> <h4 class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink m-0 mb-[14px]">Categories</h4>
<p><a href="/categories/gothic/">Gothic</a></p> <p><a class="hover:text-ink" href="/categories/gothic/">Gothic</a></p>
<p><a href="/categories/cyberpunk/">Cyberpunk</a></p> <p><a class="hover:text-ink" href="/categories/cyberpunk/">Cyberpunk</a></p>
<p><a href="/categories/dark-fantasy/">Dark Fantasy</a></p> <p><a class="hover:text-ink" href="/categories/dark-fantasy/">Dark Fantasy</a></p>
<p><a href="/categories/sci-fi/">Sci-Fi</a></p> <p><a class="hover:text-ink" href="/categories/sci-fi/">Sci-Fi</a></p>
<p><a href="/categories/cultural/">Cultural</a></p> <p><a class="hover:text-ink" href="/categories/cultural/">Cultural</a></p>
</div> </div>
<div> <div>
<h4>Index</h4> <h4 class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink m-0 mb-[14px]">Index</h4>
<p><a href="/tags/cape/">Cape</a></p> <p><a class="hover:text-ink" href="/tags/cape/">Cape</a></p>
<p><a href="/tags/neon/">Neon</a></p> <p><a class="hover:text-ink" href="/tags/neon/">Neon</a></p>
<p><a href="/tags/rain/">Rain</a></p> <p><a class="hover:text-ink" href="/tags/rain/">Rain</a></p>
<p><a href="/tags/gothic/">Gothic</a></p> <p><a class="hover:text-ink" href="/tags/gothic/">Gothic</a></p>
<p><a href="/tags/warrior/">Warrior</a></p> <p><a class="hover:text-ink" href="/tags/warrior/">Warrior</a></p>
</div> </div>
<div> <div>
<h4>Colophon</h4> <h4 class="font-sans font-medium text-[10px] leading-none tracking-[.22em] uppercase text-ink m-0 mb-[14px]">Colophon</h4>
<p>Set in Italiana &amp; Cormorant Garamond, with Outfit for typographic furniture. © Roux MMXXVI.</p> <p>Set in Italiana &amp; Cormorant Garamond, with Outfit for typographic furniture. © Roux MMXXVI.</p>
<p style="margin-top:14px;color:var(--ink)">Press <span style="border:1px solid var(--rule);padding:3px 6px;border-radius:3px;font-family:var(--mono);font-size:10px">⌘K</span> from anywhere to search.</p> <p class="mt-[14px] text-ink">Press
<span class="border border-[var(--rule)] px-[6px] py-[3px] rounded-[3px] font-mono text-[10px]">⌘K</span>
from anywhere to search.
</p>
</div> </div>
</footer> </footer>
+56 -22
View File
@@ -1,38 +1,72 @@
{{- $cats := slice "Gothic" "Cyberpunk" "Dark Fantasy" "Urban" "Noir" "Fantasy" "Cultural" "Sci-Fi" "Boudoir" "Steampunk" "Luxury" "Action" "Lifestyle" "Nature" "Romantic" "Nightlife" "Elegant" "Adventure" "Mythology" }} {{- $cats := slice "Gothic" "Cyberpunk" "Dark Fantasy" "Urban" "Noir" "Fantasy" "Cultural" "Sci-Fi" "Boudoir" "Steampunk" "Luxury" "Action" "Lifestyle" "Nature" "Romantic" "Nightlife" "Elegant" "Adventure" "Mythology" }}
<header class="masthead" id="masthead"> <header id="masthead"
<div class="masthead__inner"> class="sticky top-0 z-50 border-b border-[var(--rule)]
<div class="masthead__left"> backdrop-blur-[14px] saturate-[1.05]
<span class="masthead__date" id="mhDate"></span> [background:color-mix(in_oklab,var(--paper)_86%,transparent)]">
<span class="mh-issue">№ {{ .Site.Params.issueNumber }}</span>
<span class="mh-sep" style="opacity:.5">·</span> {{/* ── three-column top row ── */}}
<span class="mh-pub">Roux Quarterly</span> <div class="grid items-center gap-[clamp(12px,2vw,24px)] px-[var(--pad)] py-[14px]
[grid-template-columns:1fr_auto_1fr]
max-[620px]:py-3 max-[460px]:[grid-template-columns:1fr]">
{{/* left meta */}}
<div class="flex items-center gap-[18px] text-[11px] tracking-[.14em] uppercase text-ink-soft min-w-0
max-[620px]:gap-[10px] max-[620px]:text-[10px] max-[460px]:hidden">
<span id="mhDate" class="tabular-nums whitespace-nowrap"></span>
<span class="whitespace-nowrap max-[1100px]:hidden">№ {{ .Site.Params.issueNumber }}</span>
<span class="opacity-50 whitespace-nowrap max-[820px]:hidden">·</span>
<span class="whitespace-nowrap max-[820px]:hidden">Roux Quarterly</span>
</div> </div>
<a class="masthead__logo" href="/" aria-label="Roux — home"> {{/* logo (center) */}}
<a class="block leading-none text-ink justify-self-center" href="/" aria-label="Roux — home">
{{- partial "logo.html" . }} {{- partial "logo.html" . }}
</a> </a>
<div class="masthead__right"> {{/* right nav */}}
<a href="/issues/" class="mh-link">Issues</a> <div class="flex items-center justify-end gap-[18px] text-[11px] tracking-[.14em] uppercase text-ink-soft min-w-0
<span class="mh-sep" style="opacity:.5">·</span> max-[620px]:gap-[10px] max-[620px]:text-[10px] max-[460px]:hidden">
<span class="mh-city">Paris</span> <a href="/issues/" class="nav-link">Issues</a>
<span class="opacity-50 max-[820px]:hidden">·</span>
<span class="whitespace-nowrap max-[1100px]:hidden">Paris</span>
</div> </div>
</div> </div>
<div class="subhead" style="position:relative"> {{/* ── search bar ── */}}
<label class="subhead__search" for="searchInput"> <div class="relative flex items-stretch border-t border-[var(--rule-2)] border-b border-[var(--rule)]
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg> [background:color-mix(in_oklab,var(--paper)_95%,transparent)]">
<input id="searchInput" type="search" autocomplete="off" placeholder="Search the archive — silk, nocturne, atelier…" /> <label class="flex-1 flex items-center gap-3 px-[var(--pad)] py-[10px] border-r border-[var(--rule-2)] min-w-0 cursor-text"
<span class="subhead__kbd">⌘ K</span> for="searchInput">
<svg class="shrink-0 text-ink-soft" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input id="searchInput" type="search" autocomplete="off"
placeholder="Search the archive — silk, nocturne, atelier…"
class="flex-1 min-w-0 border-0 bg-transparent outline-none
font-serif font-normal italic text-[16px] leading-none text-ink tracking-[.005em] py-1.5
placeholder:text-ink-soft placeholder:opacity-90
max-[720px]:text-[15px]" />
<span class="shrink-0 font-sans font-medium text-[10px] leading-none tracking-[.12em] uppercase
border border-[var(--rule)] px-[7px] py-[5px] rounded-[3px] text-ink-soft
max-[720px]:hidden">⌘ K</span>
</label> </label>
<div class="subhead__count" id="count"></div> <div id="count"
<div class="searchpop" id="searchpop"></div> class="flex items-center px-[var(--pad)]
font-sans font-medium text-[11px] leading-none tracking-[.16em] uppercase
text-ink-soft tabular-nums whitespace-nowrap
max-[720px]:hidden [&_b]:text-ink [&_b]:font-semibold [&_b]:mr-1"></div>
<div id="searchpop" class="searchpop"></div>
</div> </div>
<div class="tabs" id="tabs"> {{/* ── category tabs ── */}}
<button data-cat="All" aria-pressed="true">All</button> <div id="tabs" class="tabs flex gap-1 overflow-x-auto px-[var(--pad)] py-[10px] border-b border-[var(--rule-2)] bg-[var(--paper)] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
<button data-cat="All" aria-pressed="true"
class="shrink-0 px-[14px] py-2 rounded-full border border-transparent
font-sans font-medium text-[11px] leading-none tracking-[.14em] uppercase
text-ink-soft transition-[color,border-color,background] duration-200 hover:text-ink">All</button>
{{- range $cats }} {{- range $cats }}
<button data-cat="{{ . }}" aria-pressed="false">{{ . }}</button> <button data-cat="{{ . }}" aria-pressed="false"
class="shrink-0 px-[14px] py-2 rounded-full border border-transparent
font-sans font-medium text-[11px] leading-none tracking-[.14em] uppercase
text-ink-soft transition-[color,border-color,background] duration-200 hover:text-ink">{{ . }}</button>
{{- end }} {{- end }}
</div> </div>
</header> </header>
+10 -10
View File
@@ -1,6 +1,6 @@
<div class="lb" id="lb" data-open="false" role="dialog" aria-modal="true" aria-label="Roux plate viewer"> <div class="lb" id="lb" data-open="false" role="dialog" aria-modal="true" aria-label="Roux plate viewer">
<div class="lb__topbar"> <div class="lb-topbar">
<div class="lb__brand"> <div class="lb-brand">
<svg viewBox="0 0 64 64" aria-hidden="true" style="height:24px;width:24px;flex:0 0 auto"> <svg viewBox="0 0 64 64" aria-hidden="true" style="height:24px;width:24px;flex:0 0 auto">
<defs><radialGradient id="lbBloom" cx="50%" cy="46%" r="58%"><stop offset="0" stop-color="#b34a30"/><stop offset=".6" stop-color="#8a3322"/><stop offset="1" stop-color="#5a1d15"/></radialGradient></defs> <defs><radialGradient id="lbBloom" cx="50%" cy="46%" r="58%"><stop offset="0" stop-color="#b34a30"/><stop offset=".6" stop-color="#8a3322"/><stop offset="1" stop-color="#5a1d15"/></radialGradient></defs>
<g fill="#6e2519" transform="translate(32 32)"><ellipse cx="0" cy="-16" rx="10.5" ry="14"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(60)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(120)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(180)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(240)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(300)"/></g> <g fill="#6e2519" transform="translate(32 32)"><ellipse cx="0" cy="-16" rx="10.5" ry="14"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(60)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(120)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(180)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(240)"/><ellipse cx="0" cy="-16" rx="10.5" ry="14" transform="rotate(300)"/></g>
@@ -12,20 +12,20 @@
<span style="font:400 22px/1 'Italiana',serif;letter-spacing:.04em;color:#f5f0e6">Roux</span> <span style="font:400 22px/1 'Italiana',serif;letter-spacing:.04em;color:#f5f0e6">Roux</span>
<span style="opacity:.55">№ {{ .Site.Params.issueNumber }} · The Plates</span> <span style="opacity:.55">№ {{ .Site.Params.issueNumber }} · The Plates</span>
</div> </div>
<div class="lb__index" id="lbIndex">000 / 100</div> <div class="lb-index" id="lbIndex">000 / 100</div>
<button class="lb__close" data-act="close" aria-label="Close"> <button class="lb-close" data-act="close" aria-label="Close">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button> </button>
</div> </div>
<div class="lb__stage"> <div class="lb-stage">
<button class="lb__nav lb__nav--prev" data-act="prev" aria-label="Previous"> <button class="lb-nav lb-nav-prev" data-act="prev" aria-label="Previous">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
</button> </button>
<div class="lb__track" id="lbTrack"></div> <div class="lb-track" id="lbTrack"></div>
<button class="lb__nav lb__nav--next" data-act="next" aria-label="Next"> <button class="lb-nav lb-nav-next" data-act="next" aria-label="Next">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg> <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 6 6 6-6 6"/></svg>
</button> </button>
</div> </div>
<aside class="lb__meta" id="lbMeta"></aside> <aside class="lb-meta" id="lbMeta"></aside>
<div class="lb__thumbs" id="lbThumbs"></div> <div class="lb-thumbs" id="lbThumbs"></div>
</div> </div>
+4 -4
View File
@@ -1,5 +1,5 @@
<span class="logo"> <span class="grid justify-items-center gap-1 text-center">
<svg class="logo__mark" viewBox="0 0 64 64" aria-hidden="true"> <svg class="block w-[30px] h-[30px] max-[620px]:w-[22px] max-[620px]:h-[22px]" viewBox="0 0 64 64" aria-hidden="true">
<defs> <defs>
<radialGradient id="mhBloom" cx="50%" cy="46%" r="58%"> <radialGradient id="mhBloom" cx="50%" cy="46%" r="58%">
<stop offset="0" stop-color="#b34a30"/> <stop offset="0" stop-color="#b34a30"/>
@@ -33,6 +33,6 @@
<circle cx="32" cy="32" r="3.2" fill="#3a120c"/> <circle cx="32" cy="32" r="3.2" fill="#3a120c"/>
<circle cx="32" cy="31.4" r="1.2" fill="#c0573c" opacity=".7"/> <circle cx="32" cy="31.4" r="1.2" fill="#c0573c" opacity=".7"/>
</svg> </svg>
<span class="logo__word">Roux</span> <span class="font-display font-normal text-[30px] leading-none tracking-[.045em] text-ink max-[620px]:text-[22px]">Roux</span>
<span class="logo__tag">Le Journal · Paris</span> <span class="font-sans font-medium text-[8.5px] leading-none tracking-[.32em] uppercase text-ink-soft max-[620px]:text-[7.5px] max-[620px]:tracking-[.28em]">Le Journal · Paris</span>
</span> </span>
+35 -37
View File
@@ -129,16 +129,16 @@
const cats = allCats(); const cats = allCats();
const tags = allTags(); const tags = allTags();
searchPop.innerHTML = ` searchPop.innerHTML = `
<div class="searchpop__section"> <div class="sp-section">
<div class="searchpop__label">Categories <small>${cats.length}</small></div> <div class="sp-label">Categories <small>${cats.length}</small></div>
<div class="searchpop__chips"> <div class="sp-chips">
${cats.map(c => `<button class="searchpop__chip" data-jump="/categories/${encodeURIComponent(c.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> ${esc(c)}</button>`).join('')} ${cats.map(c => `<button class="sp-chip" data-jump="/categories/${encodeURIComponent(c.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> ${esc(c)}</button>`).join('')}
</div> </div>
</div> </div>
<div class="searchpop__section"> <div class="sp-section">
<div class="searchpop__label">Popular tags <small>${tags.length}</small></div> <div class="sp-label">Popular tags <small>${tags.length}</small></div>
<div class="searchpop__chips"> <div class="sp-chips">
${tags.map(t => `<button class="searchpop__chip" data-jump="/tags/${encodeURIComponent(t.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> # ${esc(t)}</button>`).join('')} ${tags.map(t => `<button class="sp-chip" data-jump="/tags/${encodeURIComponent(t.toLowerCase().replace(/[\s,]+/g,'-').replace(/[^a-z0-9-]/g,''))}/"> # ${esc(t)}</button>`).join('')}
</div> </div>
</div>`; </div>`;
searchPop.dataset.open = 'true'; searchPop.dataset.open = 'true';
@@ -147,17 +147,17 @@
const hits = INDEX(q) || []; const hits = INDEX(q) || [];
const terms = q.toLowerCase().split(/\s+/).filter(t => t.length > 1); const terms = q.toLowerCase().split(/\s+/).filter(t => t.length > 1);
if (!hits.length) { if (!hits.length) {
searchPop.innerHTML = `<div class="searchpop__section"><p style="color:var(--ink-soft);font-size:13px;font-style:italic">No plates match — try <em>gothic</em>, <em>warrior</em>, or <em>neon</em>.</p></div>`; searchPop.innerHTML = `<div class="sp-section"><p style="color:var(--ink-soft);font-size:13px;font-style:italic">No plates match — try <em>gothic</em>, <em>warrior</em>, or <em>neon</em>.</p></div>`;
searchPop.dataset.open = 'true'; searchPop.dataset.open = 'true';
return; return;
} }
const shown = hits.slice(0, 6); const shown = hits.slice(0, 6);
searchPop.innerHTML = ` searchPop.innerHTML = `
<div class="searchpop__section"> <div class="sp-section">
<div class="searchpop__label">Plates <small>${hits.length}</small></div> <div class="sp-label">Plates <small>${hits.length}</small></div>
<div class="searchpop__hits"> <div class="sp-hits">
${shown.map(p => ` ${shown.map(p => `
<button class="searchpop__hit" data-jump="${esc(p.url)}"> <button class="sp-hit" data-jump="${esc(p.url)}">
${p.thumb ? `<img src="${esc(p.thumb)}" alt="" loading="lazy" />` : '<div style="width:44px;height:66px;background:var(--paper-2)"></div>'} ${p.thumb ? `<img src="${esc(p.thumb)}" alt="" loading="lazy" />` : '<div style="width:44px;height:66px;background:var(--paper-2)"></div>'}
<div> <div>
<div class="t">${highlight(p.title, terms)}</div> <div class="t">${highlight(p.title, terms)}</div>
@@ -241,14 +241,13 @@
if (!lbTrack) return; if (!lbTrack) return;
lbTrack.innerHTML = lbList.map((p, i) => { lbTrack.innerHTML = lbList.map((p, i) => {
const imgSrc = p.card || p.thumb || ''; const imgSrc = p.card || p.thumb || '';
return `<div class="lb__slide" data-i="${i}"> return `<div class="lb-slide" data-i="${i}">
<div class="lb__frame"> <div class="lb-frame">
<img class="lb__img" src="${esc(imgSrc)}" alt="${esc(p.title)}" loading="lazy" /> <img class="lb-img" src="${esc(imgSrc)}" alt="${esc(p.title)}" loading="lazy" />
</div> </div>
</div>`; </div>`;
}).join(''); }).join('');
lbBuilt = true; lbBuilt = true;
// Preload neighbors
preloadNeighbors(lbIdx); preloadNeighbors(lbIdx);
} }
@@ -271,7 +270,7 @@
[-1, 0, 1, 2].forEach(d => { [-1, 0, 1, 2].forEach(d => {
const ni = idx + d; const ni = idx + d;
if (ni < 0 || ni >= lbList.length) return; if (ni < 0 || ni >= lbList.length) return;
const slide = lbTrack.querySelector(`.lb__slide[data-i="${ni}"]`); const slide = lbTrack.querySelector(`.lb-slide[data-i="${ni}"]`);
if (!slide) return; if (!slide) return;
const img = slide.querySelector('img'); const img = slide.querySelector('img');
if (img && !img.src) img.src = lbList[ni].card || lbList[ni].thumb || ''; if (img && !img.src) img.src = lbList[ni].card || lbList[ni].thumb || '';
@@ -283,22 +282,22 @@
const cats = (p.categories || []).join(', '); const cats = (p.categories || []).join(', ');
const tags = (p.tags || []).slice(0, 8); const tags = (p.tags || []).slice(0, 8);
lbMeta.innerHTML = ` lbMeta.innerHTML = `
<div class="lb__cat">${esc(cats)}</div> <div class="lb-cat">${esc(cats)}</div>
<h2 class="lb__title">${esc(p.title)}</h2> <h2 class="lb-title">${esc(p.title)}</h2>
<p class="lb__desc">${esc(p.description)}</p> <p class="lb-desc">${esc(p.description)}</p>
<dl class="lb__factgrid"> <dl class="lb-facts">
<div class="lb__fact"><dt>Plate</dt><dd>№ ${esc(p.id)}</dd></div> <div class="lb-fact"><dt>Plate</dt><dd>№ ${esc(p.id)}</dd></div>
<div class="lb__fact"><dt>Issue</dt><dd><a href="/issues/${esc(p.issue || '01')}/" style="color:inherit;border-bottom:1px solid currentColor">№ ${esc(p.issue || '01')}</a></dd></div> <div class="lb-fact"><dt>Issue</dt><dd><a href="/issues/${esc(p.issue || '01')}/" style="color:inherit;border-bottom:1px solid currentColor">№ ${esc(p.issue || '01')}</a></dd></div>
<div class="lb__fact"><dt>Category</dt><dd>${esc((p.categories||['—'])[0])}</dd></div> <div class="lb-fact"><dt>Category</dt><dd>${esc((p.categories||['—'])[0])}</dd></div>
</dl> </dl>
<div class="lb__tags">${tags.map(t => { <div class="lb-tags">${tags.map(t => {
const slug = t.toLowerCase().replace(/[\s,]+/g, '-').replace(/[^a-z0-9-]/g, ''); const slug = t.toLowerCase().replace(/[\s,]+/g, '-').replace(/[^a-z0-9-]/g, '');
return `<a class="lb__tag" href="/tags/${slug}/"># ${esc(t)}</a>`; return `<a class="lb-tag" href="/tags/${slug}/"># ${esc(t)}</a>`;
}).join('')}</div> }).join('')}</div>
<div class="lb__share"> <div class="lb-share">
<button class="lb__sh lb__sh--primary" id="lbCopy" data-url="${esc(p.url)}"> <button class="lb-sh lb-sh-primary" id="lbCopy" data-url="${esc(p.url)}">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 1 0-7.07-7.07l-1 1"/><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 1 0 7.07 7.07l1-1"/></svg> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 1 0-7.07-7.07l-1 1"/><path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 1 0 7.07 7.07l1-1"/></svg>
<span class="lb__sh-l">Copy link</span> <span class="lb-sh-l">Copy link</span>
</button> </button>
</div>`; </div>`;
@@ -306,8 +305,8 @@
const url = new URL(this.dataset.url, location.origin).href; const url = new URL(this.dataset.url, location.origin).href;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
this.classList.add('is-ok'); this.classList.add('is-ok');
this.querySelector('.lb__sh-l').textContent = 'Copied!'; this.querySelector('.lb-sh-l').textContent = 'Copied!';
setTimeout(() => { this.classList.remove('is-ok'); this.querySelector('.lb__sh-l').textContent = 'Copy link'; }, 2000); setTimeout(() => { this.classList.remove('is-ok'); this.querySelector('.lb-sh-l').textContent = 'Copy link'; }, 2000);
}); });
}); });
} }
@@ -315,22 +314,22 @@
function lbBuildThumbs() { function lbBuildThumbs() {
if (!lbThumbs) return; if (!lbThumbs) return;
lbThumbs.innerHTML = lbList.map((p, i) => lbThumbs.innerHTML = lbList.map((p, i) =>
`<button class="lb__thumb" data-i="${i}" aria-current="${i === lbIdx}" aria-label="${esc(p.title)}"> `<button class="lb-thumb" data-i="${i}" aria-current="${i === lbIdx}" aria-label="${esc(p.title)}">
<img src="${esc(p.thumb || p.card || '')}" alt="" loading="lazy" /> <img src="${esc(p.thumb || p.card || '')}" alt="" loading="lazy" />
</button>` </button>`
).join(''); ).join('');
lbThumbs.addEventListener('click', e => { lbThumbs.addEventListener('click', e => {
const btn = e.target.closest('.lb__thumb'); const btn = e.target.closest('.lb-thumb');
if (btn) goToSlide(parseInt(btn.dataset.i)); if (btn) goToSlide(parseInt(btn.dataset.i));
}); });
} }
function syncThumbs() { function syncThumbs() {
if (!lbThumbs) return; if (!lbThumbs) return;
lbThumbs.querySelectorAll('.lb__thumb').forEach((b, i) => { lbThumbs.querySelectorAll('.lb-thumb').forEach((b, i) => {
b.setAttribute('aria-current', i === lbIdx ? 'true' : 'false'); b.setAttribute('aria-current', i === lbIdx ? 'true' : 'false');
}); });
const active = lbThumbs.querySelector('.lb__thumb[aria-current="true"]'); const active = lbThumbs.querySelector('.lb-thumb[aria-current="true"]');
if (active) active.scrollIntoView({ inline: 'center', behavior: 'smooth' }); if (active) active.scrollIntoView({ inline: 'center', behavior: 'smooth' });
} }
@@ -432,7 +431,6 @@
syncTabs(); syncTabs();
// If new page has a slug to open // If new page has a slug to open
const script = doc.querySelector('script[data-open-slug]');
const openSlug = window.__ROUX_OPEN_SLUG = doc.querySelector('[data-open-slug]')?.dataset.openSlug || null; const openSlug = window.__ROUX_OPEN_SLUG = doc.querySelector('[data-open-slug]')?.dataset.openSlug || null;
if (openSlug) { if (openSlug) {
const opened = POSTS.find(p => p.slug === openSlug); const opened = POSTS.find(p => p.slug === openSlug);