From a515b9685a7ecab4a6fe8386db48f931f72cf905 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 5 Sep 2026 19:15:20 +0000 Subject: [PATCH 1/4] Fill the blog covers with a gradient and a grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cover was a flat brand-600 rect behind the artwork, the same fill in both colour modes. It now runs a vertical gradient from a shade just off brand-500 down to brand-900, with a faint grid over it, masked out towards the edges so the centre carries the picture. The fill has to be an SVG paint server: `fill` on an SVG shape takes `url(#id)` and nothing else, so a CSS gradient cannot reach it. The def is emitted once per cover and keyed by slug, because SVG ids are document-global and a listing page shows several covers at once — one shared id would paint every cover from whichever parsed first. The id travels to the scoped stylesheet through a `--cover-bg` custom property, which a scoped rule can read and could not otherwise interpolate. The stops sit at 0.1296 and 0.8704 rather than 0 and 1. The artwork is scaled 1.35 inside its frame, so the frame shows the middle slice and never the SVG's own top or bottom edge; stops at the extremes would put both ends of the ramp in the cropped remainder, leaving the visible slice with neither end colour. They are computed from the same constant as the transform. Moving the gradient vector out of range instead is valid SVG and renders in Chromium, but not on iOS. The grid is sized in percentages, not pixels: sixteen columns at every width, so a cover reads the same in a card as across an article hero. The cells come out square because every cover is 1200x630, which all 25 in `src/assets/blog/` are. One `.bg` rule serves both modes. A flat fill for light and another for dark would sit after it at equal specificity and win on source order, handing light mode its flat background back. Checked in all twelve colour themes: each resolves a distinct pair of stops, and no cover falls back to black. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BwJB1u6U3vdEZ9LrHRebST --- src/components/blog/BlogImageSVG.astro | 114 +++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 5 deletions(-) diff --git a/src/components/blog/BlogImageSVG.astro b/src/components/blog/BlogImageSVG.astro index 58a981d7b..a6d2c46a2 100644 --- a/src/components/blog/BlogImageSVG.astro +++ b/src/components/blog/BlogImageSVG.astro @@ -8,10 +8,58 @@ interface Props { const { slug, title } = Astro.props; const svgContent = svgs[`/src/assets/blog/${slug}.svg`] ?? ''; + +/** + * Where the gradient's two stops sit, in the rect's own coordinates. + * + * The artwork is scaled inside its frame, so the frame shows the middle slice + * of the SVG and never its top or bottom edge. Stops at 0 and 1 would put both + * ends of the gradient in the part that is cropped away, and the visible slice + * would show neither the light colour nor the dark one. So they sit on the + * edges of the visible slice instead, and `spreadMethod` (pad, the default) + * fills the cropped remainder with the end colours. + * + * Both numbers stay inside 0..1. Moving the gradient VECTOR out of range + * instead — y1 below zero — is valid SVG and renders correctly in Chromium, + * but not on iOS. + * + * COVER_SCALE has to match the `scale()` in the stylesheet below. + */ +const COVER_SCALE = 1.35; +const visible = 1 / COVER_SCALE; +const stopTop = +((1 - visible) / 2).toFixed(4); +const stopFoot = +(1 - stopTop).toFixed(4); --- {svgContent && ( -