diff --git a/docs/solutions/ui-bugs/grav-cropresize-vs-cropzoom-for-cover-images.md b/docs/solutions/ui-bugs/grav-cropresize-vs-cropzoom-for-cover-images.md
new file mode 100644
index 0000000..fa6dc35
--- /dev/null
+++ b/docs/solutions/ui-bugs/grav-cropresize-vs-cropzoom-for-cover-images.md
@@ -0,0 +1,120 @@
+---
+title: "Grav cropResize fits-inside, not crop-to-fill — blurry cover/banner images"
+date: 2026-07-07
+category: ui-bugs
+module: intotheeast-theme
+problem_type: ui_bug
+component: rails_view
+symptoms:
+ - "Trip banner/cover renders blurry and badly cropped even though the source photo looks high-res in the post"
+ - "A portrait phone photo appears as a thin, upscaled horizontal sliver in a wide banner strip"
+ - "Cover derivative comes back at the source aspect ratio (e.g. 165x220 from a 1013x1350 portrait) instead of the requested strip"
+root_cause: wrong_api
+resolution_type: code_fix
+severity: medium
+tags: [grav, twig, medium, cropresize, cropzoom, srcset, retina, cover-image, object-fit]
+---
+
+# Grav cropResize fits-inside, not crop-to-fill — blurry cover/banner images
+
+## Problem
+
+The shared trip-cover macro produced a blurry, badly-composed banner/card image
+for any trip whose cover fell back to a portrait journal photo. It looked like a
+low-quality source, but the source was fine — the wrong Grav Medium operation was
+turning it into a tiny sliver that CSS then upscaled.
+
+## Symptoms
+
+- Trip banner on `/trips/us-canada-mex-2024` looked "horrendous" — soft and
+ zoomed — while the same photo looked sharp inside the journal post.
+- The rendered `
` derivative came back at the *source* aspect ratio, not the
+ requested strip: `cropResize(720, 220)` on a 1013×1350 portrait produced a
+ **165×220** image (0.75 ratio, matching the source), not a 720×220 strip.
+- The banner box (`.trip-header-banner img { object-fit: cover; height: 200px }`)
+ then upscaled that ~165px-wide sliver ~4× to fill the column → blur.
+
+## What Didn't Work
+
+- **Assuming it was source/image quality.** The imported photos are only
+ ~700–1200px wide (pixelfed served downscaled web renditions), but that alone
+ did not explain the blur — the same file was sharp in the post.
+- **Capping the derivative width to avoid upscaling (`min(w, source_width)`), as
+ a first pass.** This stopped Grav from re-encoding an upscaled JPEG, but the
+ derivative was *still* a portrait sliver because `cropResize` was still the
+ wrong operation — it emitted odd intermediate `srcset` widths (`1013w`,
+ `1200w`) without fixing the composition. It was treating a symptom.
+
+## Solution
+
+Switch the cover operation from `cropResize` (fit-inside) to `cropZoom`
+(crop-to-fill / cover), and make retina all-or-nothing so a narrow source is
+never upscaled.
+
+```twig
+{# BEFORE — cropResize fits the source INSIDE the box, preserving its aspect
+ ratio, so a portrait comes back as a narrow sliver #}
+
+
+{# AFTER — cropZoom crops-to-fill, returning an actual w×h cover strip; the 2x
+ descriptor is emitted only when the source is genuinely >= 2w wide #}
+
+```
+
+Verified empirically against the running container (do not trust the method
+names from memory — Grav's op semantics are non-obvious):
+
+| op | on a 1013×1350 portrait, target 720×220 | shape |
+|----|------------------------------------------|-------|
+| `cropResize(720, 220)` | **165×220** | fit-inside (source aspect kept) |
+| `cropZoom(720, 220)` | **720×220** | crop-to-fill (cover) ✅ |
+| `resize(720, 220)` | 720×220 | stretched/distorted ✗ |
+
+## Why This Works
+
+Grav's `Medium::cropResize($w, $h)` scales the image to **fit inside** the
+`$w × $h` box while preserving the source aspect ratio — for a tall portrait it
+is bound by height, yielding a narrow image far smaller than `$w`. `cropZoom`
+instead scales to **cover** the box and crops the overflow, so it always returns
+exactly `$w × $h` with no distortion. A banner/card strip wants cover behavior,
+so `cropZoom` is correct. Capping widths at `cover.width` prevents Grav from
+re-encoding an upscaled derivative; combined with `object-fit: cover` on the
+element, the browser gets a sharp strip at (or below) native resolution.
+
+Note the imported photos cap at ~1440px wide, so `cover.width >= 2w` is usually
+false for the wide banner — auto-picked covers render 1x-only (sharp on standard
+displays; retina only engages for an explicitly-set wide landscape `cover_image`).
+
+## Prevention
+
+- **Choose the Grav Medium op by intent, and verify the output dimensions.**
+ For a fixed-shape strip/thumbnail (banner, card, avatar) use `cropZoom`
+ (crop-to-fill). Use `cropResize` only when you actually want the whole image
+ fit inside a bounding box (aspect preserved, letterbox-friendly).
+- **Confirm Medium API behavior empirically before shipping** rather than trusting
+ method names — a quick `php bin/grav` script that runs the op and calls
+ `getimagesize()` on the derivative catches fit-vs-fill surprises. (auto memory
+ [claude]: this repo's standing guidance is to look up / verify Grav + plugin
+ API behavior, never guess it.)
+- **Guard retina descriptors against upscaling:** only add the 2x `srcset`
+ candidate when `cover.width >= 2 * targetWidth`; never emit a derivative wider
+ than the source.
+- **Regression test the composition, not just the URL.** Assert the loaded
+ banner image's natural aspect ratio is the wide strip ratio (e.g. `nw/nh > 3`),
+ which fails if a future edit reverts to a fit-inside sliver. See
+ `tests/ui/trip/trip-header.spec.js` (portrait-source regression on
+ `us-canada-mex-2024`).
+
+## Related Issues
+
+- Feature that introduced the macro: `docs/working/plans/2026-07-05-trip-description-and-hero.md`
+ (see the 2026-07-07 follow-up note). Session history shows the retina cover
+ macro was built entirely with `cropResize` across the feature sessions and
+ `cropZoom` was never evaluated, so the bug was latent from inception and only
+ surfaced when real portrait content hit the banner. (session history)
+- Backlog: full-resolution re-import of pixelfed photos — `docs/working/backlog.md`
+ (Content quality — luxury). The ~1440px source ceiling is why auto covers are
+ 1x-only.