site: layout width tiers + masonry glide — column changes animate instead of snapping

Named width tiers (mobile/narrow/regular/wide) with body max-width easing
between them; grid pages opt in via BaseLayout `wide`. Masonry is now
absolute-positioned with FLIP glides on column-count changes: targets are
computed from the *final* container width (reading --max, which flips
instantly while max-width tweens), mid-glide column changes retarget
instead of being dropped, and container height eases so content below
rides along.

Claude-Session: https://claude.ai/code/session_01ADR9r5rw5NuvvnCxfYbMuc
This commit is contained in:
2026-07-11 13:42:23 -04:00
parent d51dd85a36
commit e06e7311d6
7 changed files with 260 additions and 80 deletions
+37 -1
View File
@@ -20,6 +20,8 @@ interface Props {
modifiedTime?: Date;
articleTags?: string[];
jsonLd?: Record<string, unknown> | Record<string, unknown>[];
/** Opt into the wider content tier on roomy viewports (grid/listing pages). */
wide?: boolean;
}
const {
@@ -38,6 +40,7 @@ const {
modifiedTime,
articleTags,
jsonLd,
wide,
} = Astro.props;
const siteName = 'Unique';
const SITE_FOUNDED = new Date(2026, 4, 3);
@@ -77,7 +80,7 @@ const year = new Date().getFullYear();
/>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js" data-cf-beacon='{"token": "fc978456945c49ae90a0b7e0b891ffd2"}'></script>
</head>
<body>
<body class:list={[{ wide }]}>
<header class="masthead">
<div class="masthead-row">
<div class="masthead-stats">
@@ -158,8 +161,32 @@ const year = new Date().getFullYear();
--muted: #6b6b6b;
--accent: #b34700;
--rule: #e0dbd2;
/* Content column width. Reading pages keep this tight ("narrow") for a
comfortable line length; grid/listing pages opt into wider tiers via
<body class="wide"> (BaseLayout `wide` prop). See the tier table below. */
--max: 42rem;
}
/* ── Layout width tiers ────────────────────────────────────────────────
Named steps, shared with the masonry column logic (Masonry.astro).
Element rem = 18px (html font-size), so --max in rem renders ×18;
media-query breakpoints use the browser default (1rem = 16px).
tier viewport (mq) chrome (--max) grid card
mobile ≤ 880px 42rem / fluid 12 cols fluid
narrow 8801104px 42rem (756px) 3 cols contained 225px
regular 11041376px 55.5rem (999px) 4 cols contained 225px (= narrow)
wide ≥ 1376px 55.5rem (999px) fluid, full-bleed ~288px (≈ old 3-col)
• Chrome (masthead, hero, footer) is capped at the regular 55.5rem in
BOTH regular and wide — it never moves between them. Only the card grid
breaks out to full width in wide (the full-bleed rule is in Masonry.astro).
• regular = 4·225 + 3·gap(18) + 2·pad(22.5) = 999px, so its 4 columns
match narrow's 225px card exactly.
• wide engages ~10% past a 4-col grid of 288px cards
(4·288 + 3·18 + 2·22.5 = 1251px → ×1.10 ≈ 1376px = 86rem), then fills
the full browser width with as many ~288px cards as fit.
Reading pages omit `wide`, so they stay at narrow (42rem) at every size. */
@media (min-width: 69rem) { body.wide { --max: 55.5rem; } } /* regular + wide chrome */
* { box-sizing: border-box; }
html, body {
margin: 0;
@@ -171,10 +198,19 @@ const year = new Date().getFullYear();
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
/* Clip at the viewport (not body — the wide grid breaks out past body) so a
card mid-glide from a wider layout never flashes a horizontal scrollbar. */
html { overflow-x: clip; }
body {
max-width: var(--max);
margin: 0 auto;
padding: 1.5rem 1.25rem 2rem;
/* Glide between width tiers instead of snapping — the hero and contained
cards ride inside body, so they gently resize along with the column. */
transition: max-width 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
@media (prefers-reduced-motion: reduce) {
body { transition: none; }
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }