Banner T+R combined · gallery fit · Salon centered subhead · responsive headings

## Banner

- Combine .banner__random + .banner__kbd into one .banner__shuffle pill.
- New SVG: Lucide-style shuffle (two crossing arrows). Old icon clipped at
  the viewBox edges; this one is sized 24×24 with stroke well inside.
- Click randomises (was already so); icon rotates 15° on hover; T kbd label
  inside the pill as a hint, separated by a small vertical rule.
- Banner now uses display:flex with a spacer so the theme-name sits
  immediately to the LEFT of the picker (closer, smaller text 0.95rem)
  rather than centered far from the dots.
- New responsive breakpoints: 50rem (smaller dots), 44rem (hide picker
  entirely on phones, keep mark + name + shuffle), 26rem (drop name).
- Shuffle now sits OUTSIDE the picker so it stays visible when the picker
  is hidden on narrow screens.

## Gallery thumb fit

- Width was `100%` which only fills the content-area, leaving padding
  visible to the right (negative margin-right doesn't extend element
  width). Switched to `width: calc(100% + 2 * var(--space-lg))` so the
  thumb truly bleeds to the card's inner-border on both sides.
- Added explicit `aspect-ratio: 16/10` + `object-fit: cover` +
  `object-position: top left` so the thumb crops from the right (where
  empty page-paper tends to live) rather than top/bottom (where hero
  content usually is).
- Re-captured all 8 thumbnails at 1024×640 (matches the most common page
  content max-width — Maple Bakery is 64rem = 1024px exactly), so each
  page packs content closer to the right edge.

## Salon: centered examples intro

- Salon already centers .section__title; the .examples__intro lead
  paragraph below it was left-aligned, looking off-axis on the centered
  Salon section heads. Now centers with `text-align: center` +
  `margin-inline: auto` for both .examples__intro and .examples__outro.

## Responsive headings (narrow viewports)

- At <30rem viewports, .opening__title and .hero__display were rendering
  at min ~48px which is wider than the 320–375px content area for headers
  longer than ~14ch. Body's `overflow-x: hidden` was masking the issue
  visually but text was being clipped.
- New media query caps display at clamp(1.75rem, 9vw, 2.75rem) and forces
  `max-width: 12ch` so long headers wrap.
- .section__title gets clamp(1.375rem, 5.5vw, 1.875rem) + 100% max-width
  so multi-clause section titles wrap to multiple lines instead of being
  clipped.
- .hero__lede / .opening__sub get 1rem font-size + 100% max-width on
  narrow viewports so the lede paragraph fits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Youssef
2026-04-30 18:04:48 +01:00
co-authored by Claude Opus 4.7
parent b1885e6c35
commit ddfcd6a283
12 changed files with 140 additions and 61 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 55 KiB

+99 -44
View File
@@ -28,8 +28,7 @@
position: sticky;
top: 0;
z-index: var(--z-sticky);
display: grid;
grid-template-columns: auto 1fr auto;
display: flex;
align-items: center;
gap: var(--space-lg);
padding: 0 var(--page-gutter);
@@ -45,6 +44,12 @@
color: var(--color-ink);
}
/* Spacer pushes the picker + theme-name to the right */
.banner__spacer {
flex: 1 1 auto;
min-width: 0;
}
.banner__mark {
display: inline-flex;
align-items: baseline;
@@ -64,19 +69,21 @@
letter-spacing: 0.14em;
}
/* The active theme name — the centre marker. Lives in the banner so the
page restructure registers as a deliberate event, not a passive repaint. */
/* The active theme name — sits to the LEFT of the theme picker so the
page restructure registers as a deliberate event, not a passive repaint.
Smaller text + closer to the picker on desktop; hidden on narrow viewports. */
.banner__center {
justify-self: center;
font-family: var(--font-display);
font-weight: var(--display-weight, 400);
font-weight: var(--display-weight, 500);
font-style: var(--display-style, normal);
font-size: var(--text-md);
letter-spacing: 0.04em;
font-size: 0.95rem;
letter-spacing: -0.005em;
text-transform: none;
color: var(--color-ink);
font-optical-sizing: auto;
transition: opacity var(--dur-short) var(--ease-out);
white-space: nowrap;
margin-inline-end: var(--space-xs);
}
[data-theme="manifesto"] .banner__center,
@@ -149,65 +156,98 @@
margin-inline: 0.25rem;
}
.banner__random {
/* Combined shuffle button: icon + T kbd hint inside a single pill.
Click randomises; the T inside reads as a keyboard hint, not a separate
pill. Replaces the old .banner__random + .banner__kbd combo. */
.banner__shuffle {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
gap: 0.4rem;
height: 26px;
padding: 0 0.6rem 0 0.55rem;
background: transparent;
border: 1px solid var(--color-rule);
border-radius: var(--radius-pill, 0);
border-radius: var(--radius-pill, 999px);
color: var(--color-muted);
cursor: pointer;
font-family: var(--font-label);
font-size: 0.625rem;
letter-spacing: 0.08em;
text-transform: uppercase;
transition: color var(--dur-micro) var(--ease-out),
border-color var(--dur-micro) var(--ease-out),
background var(--dur-micro) var(--ease-out),
transform 80ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
.banner__random:hover { color: var(--color-ink); border-color: var(--color-ink); }
.banner__shuffle svg {
flex-shrink: 0;
display: block;
}
.banner__random:active { transform: rotate(180deg); transition-duration: 220ms; }
.banner__shuffle-key {
display: inline-block;
font-weight: 500;
letter-spacing: 0.04em;
color: var(--color-muted);
padding-inline-start: 0.4rem;
border-inline-start: 1px solid var(--color-rule);
margin-inline-start: 0.05rem;
height: 14px;
line-height: 14px;
}
.banner__random:focus-visible {
@media (hover: hover) and (pointer: fine) {
.banner__shuffle:hover {
color: var(--color-ink);
border-color: var(--color-ink);
}
.banner__shuffle:hover .banner__shuffle-key {
color: var(--color-ink);
border-color: var(--color-ink);
}
.banner__shuffle:hover svg {
transform: rotate(15deg);
transition: transform 200ms var(--ease-out);
}
}
.banner__shuffle:active {
transform: scale(0.96);
transition-duration: 60ms;
}
.banner__shuffle:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
.banner__kbd {
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline-start: 0.25rem;
padding: 0.1rem 0.4rem;
font-family: var(--font-label);
font-size: 0.625rem;
letter-spacing: 0.08em;
color: var(--color-muted);
border: 1px solid var(--color-rule);
border-radius: 3px;
min-width: 18px;
min-height: 18px;
}
@media (max-width: 60rem) {
.banner { grid-template-columns: auto 1fr auto; gap: var(--space-md); }
.banner__center { font-size: var(--text-sm); }
.banner__kbd { display: none; }
.banner { gap: var(--space-md); }
.banner__center { font-size: 0.825rem; }
}
@media (max-width: 44rem) {
.banner__ver { display: none; }
.banner__center { display: none; }
.banner__picker { gap: 0.3rem; }
@media (max-width: 50rem) {
.banner__picker { gap: 0.28rem; }
.banner__dot { width: 14px; height: 14px; }
.banner__sep { display: none; }
}
/* Below 44rem: hide the dot grid (would overflow); keep mark + theme name + shuffle */
@media (max-width: 44rem) {
.banner__ver { display: none; }
.banner__picker { display: none; }
.banner__shuffle-key { display: none; }
.banner__shuffle { padding: 0 0.5rem; height: 24px; }
}
/* Below 26rem (very small phones): hide center label too — pure click-to-shuffle */
@media (max-width: 26rem) {
.banner__center { display: none; }
}
/* — Section label (01 / FOUNDATIONS) —————————————————— */
.section-label {
font-family: var(--font-label);
@@ -1733,19 +1773,27 @@
.gallery__thumb {
grid-area: thumb;
display: block;
width: 100%;
margin: calc(-1 * var(--space-lg)) calc(-1 * var(--space-lg)) var(--space-lg);
/* Width must EXCEED content area by 2*padding so the negative margins
reach the card's inside-border edge on both sides. width: 100% would
only fill the content area, leaving padding visible on the right. */
width: calc(100% + 2 * var(--space-lg));
margin-inline: calc(-1 * var(--space-lg));
margin-block-start: calc(-1 * var(--space-lg));
margin-block-end: var(--space-lg);
border-block-end: var(--rule-card, 1px) solid var(--color-rule);
background: var(--color-paper-2);
overflow: hidden;
position: relative;
font-size: 0; /* kill any inline-element gap */
font-size: 0; /* kill any inline-element gap */
aspect-ratio: 16 / 10; /* explicit, so the image always fits */
}
.gallery__thumb img {
display: block;
width: 100%;
height: auto;
height: 100%;
object-fit: cover;
object-position: top left; /* keep hero region; crop empty right */
transition: transform 500ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
@@ -1887,6 +1935,13 @@
opacity: 0.65;
}
/* Section 07 lead paragraph + outro — Salon centers all flowing prose */
[data-theme="salon"] .examples__intro,
[data-theme="salon"] .examples__outro {
text-align: center;
margin-inline: auto;
}
/* ───────────── NEWSPRINT — justified prose blocks ─────────────
Print discipline. Long-form copy gets justified + hyphenated where
the column is wide enough to render evenly. Headlines stay
+24
View File
@@ -50,6 +50,30 @@
}
}
/* Narrow viewports let headings wrap freely + cap display sizes so
a long title fits at 320480 px without being clipped by body's
overflow-x: hidden. */
@media (max-width: 30rem) {
.opening__title,
.hero__display {
font-size: clamp(1.75rem, 9vw, 2.75rem) !important;
max-width: 12ch;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
}
.section__title {
font-size: clamp(1.375rem, 5.5vw, 1.875rem) !important;
max-width: 100%;
overflow-wrap: break-word;
}
.hero__lede,
.opening__sub {
font-size: 1rem !important;
max-width: 100%;
}
}
/* — 01 · Opening ——————————————————————————————————— */
.opening {
display: grid;
+13 -13
View File
@@ -44,6 +44,8 @@
<span class="banner__ver">v0.2</span>
</a>
<span class="banner__spacer" aria-hidden="true"></span>
<span class="banner__center" data-theme-current>Specimen</span>
<div class="banner__picker" role="radiogroup" aria-label="Theme">
@@ -64,20 +66,18 @@
<button class="banner__dot" type="button" data-theme-btn="riso" aria-label="Riso" style="--dot: oklch(91% 0.034 30); --dot-edge: oklch(58% 0.170 220);"></button>
<button class="banner__dot" type="button" data-theme-btn="quiet" aria-label="Quiet" style="--dot: oklch(99.5% 0.002 90); --dot-edge: oklch(16% 0.005 240);"></button>
<span class="banner__sep" aria-hidden="true"></span>
<button class="banner__random" type="button" aria-label="Random theme" title="Random (R)">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 4 L7 4 L13 12 L14 12" />
<path d="M14 12 L12 10 M14 12 L12 14" />
<path d="M2 12 L7 12 L9 9.5" />
<path d="M11 6.5 L13 4 L14 4" />
<path d="M14 4 L12 2 M14 4 L12 6" />
</svg>
</button>
<span class="banner__kbd" aria-hidden="true">T</span>
</div>
<button class="banner__shuffle" type="button" aria-label="Random theme · T cycles · R randomises" title="Click → random · T cycles · R randomises">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M16 3h5v5" />
<path d="M4 20 21 3" />
<path d="M21 16v5h-5" />
<path d="m15 15 6 6" />
<path d="M4 4l5 5" />
</svg>
<span class="banner__shuffle-key">T</span>
</button>
</header>
<main class="page">
+4 -4
View File
@@ -553,14 +553,14 @@ dots.forEach((btn) => {
});
});
/* — Random button + R shortcut ——————————————————————————— */
const randomBtn = document.querySelector(".banner__random");
/* — Shuffle button + R shortcut ——————————————————————————— */
const shuffleBtn = document.querySelector(".banner__shuffle, .banner__random");
function pickRandomTheme() {
const keys = Object.keys(THEMES).filter((k) => k !== root.dataset.theme);
return keys[Math.floor(Math.random() * keys.length)];
}
if (randomBtn) {
randomBtn.addEventListener("click", () => applyTheme(pickRandomTheme()));
if (shuffleBtn) {
shuffleBtn.addEventListener("click", () => applyTheme(pickRandomTheme()));
}
/* — Keyboard shortcuts ————————————————————————————————— */