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>
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 55 KiB |
@@ -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
|
||||
|
||||
@@ -50,6 +50,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Narrow viewports — let headings wrap freely + cap display sizes so
|
||||
a long title fits at 320–480 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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 ————————————————————————————————— */
|
||||
|
||||