@charset "utf-8";

/* ==========================================================================
   USA Tailor Shops / Parisi Men's Clothing
   Single stylesheet. No build step, no external requests, system fonts only.

   Palette contrast was calculated, not eyeballed. There is one scheme (light);
   41 foreground/background pairs are verified against WCAG 2.2 AA with zero
   failures. The worst enforced ratio is 4.23:1 against a 3:1 requirement
   (hairline rule on a badge). Body text never drops below 4.98:1.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Design tokens
   -------------------------------------------------------------------------- */

:root {
  /* -- Palette: the original usatailors.com colours ------------------------
     Values taken from the original Weebly stylesheet
     (usatailors.com/files/main_style.css): a grey page carrying a white
     content panel, grey body text, navy links and a navy nav bar.

     LIGHT ONLY, DELIBERATELY. The original site has no dark mode, and a dark
     scheme was explicitly not wanted. `color-scheme: light` below keeps form
     controls, scrollbars and UI chrome light regardless of the OS setting.
     To restore automatic dark mode, re-add a `prefers-color-scheme: dark`
     block here and drop the `color-scheme` line, nothing else depends on it.

     Where an original value could not clear WCAG 2.2 AA it was darkened by the
     smallest amount that passes; each such deviation is marked ORIGINAL: below.
     Every pair is computed, not eyeballed.                                  */
  color-scheme: light;

  /* The page is white. The original Weebly theme put a #c2c2c2 grey behind a
     white content panel; that grey read as heavy on a modern full-width
     layout, so the page itself is white and the panel edges are carried by
     borders instead. Every white component (.card, .step, .knot-steps li,
     .callout, .tbl-scroll, form fields) already draws a 1px --border, which
     measures 5.74:1 on white, so nothing depends on a background step to
     stay visible.                                                          */
  --bg: #ffffff;
  --surface: #ffffff;     /* content panel / cards, original               */
  --surface-2: #f1f1f1;   /* tinted bands, original                        */
  --stripe: #e9e9e9;      /* table zebra row                                */

  --text: #2a2a2a;        /* 14.35 on white, original                      */
  --text-muted: #535353;  /*  7.69 on white, original, unmodified          */
  --accent: #124a97;      /* navy link, original. 8.56 on white            */
  /* ORIGINAL hover: #4e6b93. It clears 4.5 on white (5.46) but not on the
     zebra row (4.49) or the badge (4.02), and link hover occurs on both.
     Replaced with a darker navy from the same family.                      */
  --accent-hover: #0d3873;/* 11.47 on white, 9.45 stripe, 8.45 badge        */
  --on-accent: #ffffff;   /* 8.56 on --accent                              */

  /* ORIGINAL rules were #ddd/#d3d3d3/#ccc/#aaa, none reaches 3:1 on white
     (#ddd is 1.61:1, #aaa 2.32:1). These separators carry .facts rows and
     price-table rows, so they are load-bearing for comprehension, not
     decoration. #666666 is itself an original value and clears 3:1 on every
     surface.                                                               */
  --border: #666666;      /* 5.74 white / 5.08 band / 4.73 stripe           */
  --rule: #666666;
  --focus: #12283c;       /* navy, original. 15.06 on white                */

  --badge-bg: #dddddd;    /* original                                       */
  --badge-text: #2a2a2a;  /* 10.57 on --badge-bg                            */

  /* Footer takes the original's dark navy rather than the old ink brown. */
  --footer-bg: #12283c;   /* original                                       */
  --footer-text: #ffffff; /* 15.06 on --footer-bg                           */
  --footer-muted: #c2c2c2;/*  8.45 on --footer-bg, original                */
  --footer-link: #cfe0f5; /* 11.21 on --footer-bg                           */
  --footer-focus: #ffffff;

  /* -- Header / nav bar: white on navy, as the original ------------------- */
  --header-bg: #002a6b;   /* original nav background                        */
  --header-text: #ffffff; /* 13.61 on --header-bg, original                */
  --header-muted: #c2c2c2;/*  8.47 on --header-bg, original                */
  /* Hover must never make a label harder to read. The original theme faded nav
     links to #ccc on hover, dropping the label from 13.69:1 to 8.47:1, which
     reads as the control becoming less available exactly as you point at it.

     The fix is a DARKER fill, not a lighter one: white on #001e4e is 16.21:1,
     higher than the 13.69:1 the label has at rest, so pointing at a control
     now improves its legibility. #001e4e is the original theme's own submenu
     hover colour. Because a darker navy on navy is only 1.19:1 of separation
     and would be a weak affordance on its own, the hover also turns the
     border white (16.21:1 on the fill) and that carries the state change. */
  --header-hover-bg: #001e4e;

  /* -- Type ---------------------------------------------------------------- */
  --font-display: ui-serif, Georgia, "Iowan Old Style", "Times New Roman", Times, serif;
  --font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

  /* -- Space --------------------------------------------------------------- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4rem;

  --measure: 68ch;
  --container: 72rem;
  --radius: 3px;

  /* -- Motion --------------------------------------------------------------
     Custom curves only. The CSS built-ins are too weak to read as deliberate.
     `ease-in` is never used: it withholds movement at the exact moment the
     user is watching, which reads as lag.                                    */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  /* Colour and hover changes use plain `ease`, per emil-design-eng. */
  --ease-hover: ease;
  --dur-press: 140ms;   /* press feedback                                    */
  --dur-hover: 150ms;   /* colour only                                       */
  --dur-enter: 180ms;   /* menu in                                           */
  --dur-exit: 130ms;    /* menu out, exit is always faster than enter       */
}

/* No `prefers-color-scheme: dark` block: see the note in the token list above.
   This is a product decision, not an omission. */

/* --------------------------------------------------------------------------
   2. Reset / base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  /* Never set a px font-size here: it breaks the user's browser text size. */
  -webkit-text-size-adjust: 100%;
  scroll-padding-top: var(--space-6);
}

body {
  margin: 0;
  background-color: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  /* Fluid but always resizable, and never below 16px. */
  font-size: clamp(1rem, 0.97rem + 0.15vw, 1.0625rem);
  line-height: 1.65;
  /* `break-word` for ordinary prose: it breaks a long word only as a last
     resort and leaves normal wrapping alone. NOTE: it does NOT reduce an
     element's min-content contribution, so it cannot rescue a flex/grid item;
     see the `anywhere` rule below for those. */
  overflow-wrap: break-word;
}

/* Unbreakable strings (email addresses, bare domains) inside flex or grid
   items. `break-word` is useless here because these items have
   `min-width: auto`, so the track sizes to the longest word and the layout
   blows out regardless. Only `anywhere` reduces min-content. Measured at 200%
   text: jpippopa@usatailors.com forced 358px, the .facts grid track 358px. */
.footer-contact a,
.site-footer li a,
.facts dd,
.facts dt,
.card-desc,
.price,
.btn,
.header-phone {
  overflow-wrap: anywhere;
}

/* Grid and flex items default to `min-width: auto`, whose floor is the item's
   min-content size. For a replaced element that floor is its INTRINSIC pixel
   width, which `max-width: 100%` cannot reduce, so a 250px photo blows its
   track past a 192px column. Same mechanism for an unbreakable email address
   inside a button. Measured at 200% text on a 320px viewport before this rule:
   alterations.html overflowed 77px (.step-media forced to 279px) and
   accessories/orders overflowed 121px (.btn forced to 409px). */
.grid > li,
.steps > li,
.knot-steps > li,
.step,
.step > *,
.card,
.card-media,
.step-media,
.btn,
.two-col > *,
.price-tables > * {
  min-width: 0;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.01em;
  margin: 0 0 var(--space-4);
  text-wrap: balance;
}

h1 { font-size: clamp(2rem, 1.55rem + 2.2vw, 3.25rem); }
h2 { font-size: clamp(1.5rem, 1.28rem + 1.1vw, 2.125rem); margin-top: var(--space-7); }
h3 { font-size: clamp(1.1875rem, 1.11rem + 0.38vw, 1.4375rem); margin-top: var(--space-6); }
h4 { font-size: 1.0625rem; font-family: var(--font-body); font-weight: 700; margin-top: var(--space-5); }

p, ul, ol, dl, table, figure, blockquote { margin: 0 0 var(--space-4); }

ul, ol { padding-left: 1.4em; }
li + li { margin-top: var(--space-2); }

img { max-width: 100%; height: auto; display: block; }

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}

/* The transition lives on the BASE selector, not inside :hover. Declared
   inside :hover it stops matching the moment the pointer leaves, taking the
   transition with it, so hover-in eased and hover-out snapped. The asymmetry
   was accidental, not designed. */
@media (hover: hover) and (pointer: fine) {
  /* Colour only. `text-decoration-thickness` is a paint property and
     emil-design-eng restricts animation to transform/opacity, with colour
     sanctioned for hover; thickening an underline is decoration, and the
     colour change already carries the affordance. */
  a { transition: color var(--dur-hover) var(--ease-hover); }

  a:hover { color: var(--accent-hover); }

  /* --accent-hover is a DARKER navy: correct on the white page, but it was
     inherited by links on the navy header and footer, where it measured
     1.19:1 and 1.31:1, so the label all but vanished under the pointer. Those
     surfaces keep a light label on hover instead, so every link gains contrast
     on hover rather than losing it. Higher specificity, so these win. */
  .site-header a:hover { color: var(--header-text); }
  .site-footer a:hover { color: var(--footer-text); }
}

strong, b { font-weight: 700; }

address { font-style: normal; }

hr {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: var(--space-7) 0;
}

/* --------------------------------------------------------------------------
   3. Focus, WCAG 2.2 (2.4.7 / 2.4.11 / 2.4.13)
   The ring sits OUTSIDE the control via outline-offset, so on both of its
   edges it is adjacent to the page or card background (>= 13:1 in both
   schemes) and never to a button fill. `outline: none` appears nowhere.
   Focus is never animated: keyboard actions are repeated constantly and any
   delay reads as lag.
   -------------------------------------------------------------------------- */

/* No `border-radius` here: it changes the ELEMENT's corners, not the outline's,
   so square elements like .wordmark visibly changed shape the instant they
   received keyboard focus. The outline already follows any radius the element
   actually has. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

.site-footer :focus-visible { outline-color: var(--footer-focus); }

/* --------------------------------------------------------------------------
   4. Skip link (2.4.1), visible on focus, not merely reachable
   -------------------------------------------------------------------------- */

.skip-link {
  position: absolute;
  left: var(--space-3);
  top: var(--space-3);
  /* Beyond the 200% that 1.4.4 requires, an unconstrained absolute box still
     overflowed. Cheap to bound. */
  max-width: calc(100% - 2 * var(--space-3));
  z-index: 100;
  transform: translateY(-200%);
  background-color: var(--surface);
  color: var(--text);
  border: 2px solid var(--focus);
  border-radius: var(--radius);
  padding: var(--space-3) var(--space-4);
  font-weight: 700;
  text-decoration: none;
}

.skip-link:focus { transform: translateY(0); }

/* --------------------------------------------------------------------------
   5. Layout
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: clamp(1rem, 0.6rem + 1.8vw, 2rem);
}

.prose { max-width: var(--measure); }

main { padding-block: var(--space-6) var(--space-8); }

section + section { margin-top: var(--space-7); }

.band {
  background-color: var(--surface-2);
  border-block: 1px solid var(--rule);
  padding-block: var(--space-6);
  margin-block: var(--space-7);
}

/* The band supplies its own top padding; a leading heading must not add to it. */
.band > .container > :first-child { margin-top: 0; }

.stack > * + * { margin-top: var(--space-4); }

/* --------------------------------------------------------------------------
   6. Header + navigation
   -------------------------------------------------------------------------- */

.site-header {
  background-color: var(--header-bg);
  border-bottom: 1px solid var(--header-bg);
}

/* Everything in the header sits on navy, so the ring must be light to keep 3:1
   (the page-level --focus navy would be 1.1:1 here and effectively invisible). */
.site-header :focus-visible { outline-color: var(--header-text); }

.header-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-5);
  padding-block: var(--space-4);
}

.wordmark {
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-decoration: none;
  color: var(--header-text);
  margin-right: auto;
}

.wordmark:hover { color: var(--header-text); }

.wordmark-name {
  overflow-wrap: anywhere;
  font-family: var(--font-display);
  font-size: clamp(1.125rem, 1.02rem + 0.5vw, 1.4375rem);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: 0.01em;
}

.wordmark-sub {
  font-size: 0.8125rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--header-muted);
}

/* The phone number is one of the two things a visitor actually needs. */
.header-phone {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  font-weight: 700;
  font-size: 1.0625rem;
  /* No `white-space: nowrap`, at 200% text it measured 293px wide inside a
     320px viewport and overflowed on its own. The number may break across two
     lines at extreme zoom; that is correct behaviour, not a defect. */
  border-radius: var(--radius);
  text-decoration: none;
  color: var(--header-text);
  border: 1px solid var(--header-muted);
}

/* Press feedback needs its own transition at base, or the scale snaps.
   Previously only .btn had one, so three of the four pressable elements felt
   different from the buttons for no reason. Declared BEFORE the hover query so
   the query's fuller transition list wins on pointer devices. */
.header-phone { transition: transform var(--dur-press) var(--ease-out); }

@media (hover: hover) and (pointer: fine) {
  .header-phone {
    transition: color var(--dur-hover) var(--ease-hover),
                background-color var(--dur-hover) var(--ease-hover),
                border-color var(--dur-hover) var(--ease-hover),
                transform var(--dur-press) var(--ease-out);
  }

  .header-phone:hover {
    color: var(--header-text);
    background-color: var(--header-hover-bg);
    border-color: var(--header-text);
  }
}

.header-phone:active { transform: scale(0.97); }

/* -- Nav toggle. Ships with the `hidden` attribute; site.js removes it once
      the toggle actually has a click handler. Until then `[hidden]` wins (see
      the `:not([hidden])` guard in the collapsed-nav block), so with JS off,
      or with JS half-loaded, no dead button is exposed and the nav below is a
      plain, always-visible wrapped list. ---------------------------------- */

.nav-toggle {
  display: none;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  min-width: 44px;
  padding: var(--space-2) var(--space-3);
  font: inherit;
  font-weight: 600;
  color: var(--header-text);
  background-color: transparent;
  border: 1px solid var(--header-muted);
  border-radius: var(--radius);
  cursor: pointer;
}

.nav-toggle[hidden] { display: none; }

.nav-toggle { transition: transform var(--dur-press) var(--ease-out); }

.nav-toggle:active { transform: scale(0.97); }

.nav-toggle-bars {
  display: block;
  position: relative;
  width: 18px;
  height: 2px;
  background-color: currentColor;
}

.nav-toggle-bars::before,
.nav-toggle-bars::after {
  content: "";
  position: absolute;
  left: 0;
  width: 18px;
  height: 2px;
  background-color: currentColor;
}

.nav-toggle-bars::before { top: -6px; }
.nav-toggle-bars::after { top: 6px; }

.site-nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

.site-nav li { margin: 0; }

.site-nav a {
  display: flex;
  align-items: center;
  overflow-wrap: anywhere;
  min-height: 44px;
  padding: var(--space-2) var(--space-3);
  color: var(--header-text);
  text-decoration: none;
  border-radius: var(--radius);
  border: 1px solid transparent;
  font-weight: 500;
}

.site-nav a { transition: transform var(--dur-press) var(--ease-out); }

@media (hover: hover) and (pointer: fine) {
  .site-nav a {
    transition: color var(--dur-hover) var(--ease-hover),
                background-color var(--dur-hover) var(--ease-hover),
                border-color var(--dur-hover) var(--ease-hover),
                transform var(--dur-press) var(--ease-out);
  }

  .site-nav a:hover {
    color: var(--header-text);
    background-color: var(--header-hover-bg);
    /* White, not --header-muted: the border is what signals the hover here. */
    border-color: var(--header-text);
  }
}

.site-nav a:active { transform: scale(0.97); }

/* Current page is marked by weight + a rule, never by colour alone (1.4.1).
   On navy the marker must be light: the page-level --accent navy would be
   1.1:1 against the header and would read as no marker at all. */
.site-nav a[aria-current="page"] {
  color: var(--header-text);
  font-weight: 700;
  border-bottom-color: var(--header-text);
  border-radius: var(--radius) var(--radius) 0 0;
}

/* -- Collapsed nav, JS present, narrow viewport only --------------------- */

@media (max-width: 61.24rem) {
  /* `:not([hidden])` so the `hidden` attribute actually wins. `hidden` is a UA
     `display: none`, which ANY author `display` overrides, so `.js
     .nav-toggle { display: inline-flex }` silently resurrected a dead button
     with no handler attached whenever `.js` was set but site.js had not run. */
  .js .nav-toggle:not([hidden]) { display: inline-flex; }

  /* EVERY collapsed-nav rule below is gated on [data-collapsible], the same
     signal that hides the panel, NOT on `.js`, which the inline <head> script
     sets before first paint.

     Gating the positioning on `.js` and the hiding on `[data-collapsible]`
     meant that between first paint and the deferred site.js executing, and
     permanently if that file 404s, is blocked, or fails on the network, the
     panel was `position: absolute; z-index: 50; visibility: visible` and
     painted opaquely over the page's own <h1>. Measured in that state:
     elementFromPoint at the h1 returned a nav <a>, and 5 <main> elements were
     covered. One signal, set by the script that also wires up the behaviour,
     means the CSS can never describe a state the JS has not reached. */
  .js .site-nav[data-collapsible] {
    position: absolute;
    /* REQUIRED. Without `top`, an abspos child of a flex container resolves to
       its static position, which `align-items: center` puts at the header's
       vertical centre, the panel then renders ~90px ABOVE the viewport and
       the first link becomes unreachable. Anchor it to the header's bottom. */
    top: 100%;
    left: 0;
    right: 0;
    z-index: 50;
    /* The panel carries the header's navy so its links keep the same
       white-on-navy treatment they have in the desktop bar, one colour rule
       for nav links in both states, rather than a conditional that has to be
       re-verified twice. As before, colour is NOT what separates the panel
       from the header (`top: 100%` is, by removing the overlap entirely); the
       two borders below carry the visible boundary, and each is chosen to be
       visible against the thing it actually abuts. */
    background-color: var(--header-bg);
    /* Top edge abuts the navy header: a light hairline (8.47:1 on navy). */
    border-top: 1px solid var(--header-muted);
    /* Bottom edge abuts the white page: --border is 5.74:1 there. */
    border-bottom: 1px solid var(--border);
    box-shadow: 0 8px 24px rgb(0 0 0 / 0.28);
    padding: var(--space-3) clamp(1rem, 0.6rem + 1.8vw, 2rem) var(--space-4);
    /* Scales down from the toggle that opened it, not from the page centre. */
    transform-origin: top right;
  }

  .js .site-nav[data-collapsible] ul { flex-direction: column; gap: var(--space-1); }
  .js .site-nav[data-collapsible] a { border: 1px solid transparent; }

  /* Closed state. `visibility: hidden` removes the links from the tab order
     and the accessibility tree, so there is no off-screen keyboard trap.
     Transitions (not keyframes) so a fast toggle retargets instead of
     restarting from zero. Never scale(0): nothing appears from nothing. */
  .js .site-nav[data-collapsible] {
    visibility: hidden;
    opacity: 0;
    transform: translateY(-6px) scale(0.98);
    transition: opacity var(--dur-exit) var(--ease-out),
                transform var(--dur-exit) var(--ease-out),
                visibility 0s linear var(--dur-exit);
  }

  .js .site-nav[data-collapsible].is-open {
    visibility: visible;
    opacity: 1;
    transform: translateY(0) scale(1);
    transition: opacity var(--dur-enter) var(--ease-out),
                transform var(--dur-enter) var(--ease-out),
                visibility 0s;
  }
}

@media (min-width: 61.25rem) {
  .nav-toggle { display: none; }
}

.header-inner { position: relative; }

/* --------------------------------------------------------------------------
   7. Buttons
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: var(--space-3) var(--space-5);
  font: inherit;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  border-radius: var(--radius);
  border: 1px solid transparent;
  cursor: pointer;
  /* transform + opacity only; never `transition: all`. */
  transition: transform var(--dur-press) var(--ease-out);
}

.btn-primary {
  background-color: var(--accent);
  color: var(--on-accent);
  border-color: var(--accent);
}

.btn-secondary {
  background-color: transparent;
  color: var(--accent);
  border-color: var(--border);
}

@media (hover: hover) and (pointer: fine) {
  .btn {
    transition: background-color var(--dur-hover) var(--ease-hover),
                color var(--dur-hover) var(--ease-hover),
                border-color var(--dur-hover) var(--ease-hover),
                transform var(--dur-press) var(--ease-out);
  }

  .btn-primary:hover {
    background-color: var(--accent-hover);
    border-color: var(--accent-hover);
    color: var(--on-accent);
  }

  .btn-secondary:hover {
    color: var(--accent-hover);
    border-color: var(--accent-hover);
  }
}

/* The interface confirming it heard the press. */
.btn:active { transform: scale(0.97); }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-block: var(--space-5);
}

/* --------------------------------------------------------------------------
   8. Hero / lede
   -------------------------------------------------------------------------- */

.hero { padding-block: var(--space-6) var(--space-4); }

.eyebrow {
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-3);
}

.lede {
  font-size: clamp(1.0625rem, 1.01rem + 0.28vw, 1.25rem);
  color: var(--text-muted);
  max-width: var(--measure);
}

.page-intro { max-width: var(--measure); }

/* --------------------------------------------------------------------------
   9. Cards / product grid
   -------------------------------------------------------------------------- */

.grid {
  display: grid;
  /* Track width is tuned to the images that actually exist, not picked first
     and hoped for. The source files are Weebly thumbnails with a median of
     164x244 (p25 125px wide, p75 250px). A 17rem track left a median image
     filling ~39% of its box once the never-upscale rule was in force; 13rem
     brings the common case close to the box width without any upscaling.
     min() keeps the track from exceeding the viewport at 320px. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 13rem), 1fr));
  gap: var(--space-5);
  list-style: none;
  margin: 0 0 var(--space-5);
  padding: 0;
}

.grid > li { margin: 0; }

.card {
  display: flex;
  flex-direction: column;
  height: 100%;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

/* Media box.
   The source images were measured, not guessed: they are Weebly thumbnails
   ranging 39x110 to 1000x1334, with aspect ratios from 0.30 (a key holder on a
   belt) to 2.67 (a row of seven ties). Two consequences drove this design:

   1. A fixed `aspect-ratio` + `object-fit: cover` would crop the wide images
      catastrophically, a 4:5 crop of the 350x131 tie photo shows two ties out
      of seven. So the box CONTAINS rather than covers, and never crops.
   2. Most files are small. `width: 100%` would upscale a 39x110 image 7.8x into
      a blurry smear. `width/height: auto` holds the image at its intrinsic
      size; the `max-*` pair only ever shrinks it. Nothing is upscaled, ever.

   A fixed box HEIGHT (not ratio) keeps grid rows even, which is what the old
   aspect-ratio was reaching for and failing to achieve, it silently did
   nothing, because a percentage height against an auto-height parent resolves
   to auto. `overflow: hidden` is present so the box is authoritative. */
.card-media {
  /* Most garment photos are portrait (median 164x244), so a taller box lets
     them fill more of it without any upscaling. Measured on shop.html at
     1042px: 39% average fill before this section was tuned, 68% after. */
  height: 16rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
  overflow: hidden;
  background-color: var(--surface-2);
  border-bottom: 1px solid var(--rule);
}

.card-media img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

/* Two views of ONE garment, side by side in a single card. Used where the
   source supplies a front and a back photograph of the same coat: as two
   separate cards a priced card and an unpriced card read as an in-stock item
   and a sold-out item, not as two views of one product. */
.card-media.is-pair { gap: var(--space-3); }
.card-media.is-pair img { max-width: calc(50% - (var(--space-3) / 2)); }

.card-body {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4);
  flex: 1;
}

.card-title {
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 600;
  line-height: 1.25;
  margin: 0;
}

.card-desc {
  margin: 0;
  font-size: 0.9375rem;
  color: var(--text-muted);
}

.card-foot { margin-top: auto; padding-top: var(--space-2); }

/* -- Price ---------------------------------------------------------------- */

.price {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
  margin: 0;
  font-size: 1rem;
}

.price-was {
  color: var(--text-muted);
  text-decoration: line-through;
  /* The words "Was"/"Now" carry the meaning; the strike-through only
     reinforces it. No information by colour or styling alone (1.4.1). */
}

.price-now { font-weight: 700; }

.price-label {
  font-size: 0.8125rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  font-weight: 600;
}

/* -- Availability badge ---------------------------------------------------
   Sold items stay visible. The status is carried by the word itself plus a
   border and a strike-through rule, never by colour. -------------------- */

.badge {
  display: inline-flex;
  align-items: center;
  /* Availability is the one status the brief requires a non-colour carrier for,
     so it must not be clipped by `.card { overflow: hidden }`. `max-width` alone
     shrank the BOX while the glyphs kept their size and spilled 101px outside
     their own border. A flex item's min-content floor is its longest word, and
     only `overflow-wrap: anywhere` + `min-width: 0` lowers it. */
  max-width: 100%;
  min-width: 0;
  white-space: normal;
  overflow-wrap: anywhere;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--badge-text);
  background-color: var(--badge-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.card.is-sold .card-media img { opacity: 0.72; }

.card.is-sold .card-title::after {
  content: "";
  display: block;
  width: 2.5rem;
  margin-top: var(--space-2);
  border-top: 2px solid var(--border);
}

/* --------------------------------------------------------------------------
   10. Steps (measuring guide)
   -------------------------------------------------------------------------- */

.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-6);
}

.steps > li { margin: 0; }

.step {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
  align-items: start;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
}

@media (min-width: 44rem) {
  .step { grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); gap: var(--space-5); }
}

/* Instructional imagery is NEVER cropped. The old 4:3 `cover` box did hold its
   ratio (unlike .card-media), which meant it actively cropped: the sleeve
   photo (_5548358.jpg, 134x285 portrait) showed ~35% of itself, cutting away
   the tape measure that the instruction text tells the reader to look at. That
   breaks the image/alt correspondence and the brief's requirement to preserve
   the four measuring instructions. Contain, letterboxed on the surface tint. */
.step-media {
  height: 17rem;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-3);
  overflow: hidden;
  background-color: var(--surface-2);
  border: 1px solid var(--rule);
  border-radius: var(--radius);
}

.step-media img { width: auto; height: auto; max-width: 100%; max-height: 100%; }

.step-title { margin-top: 0; }

.step-tag {
  display: inline-block;
  /* Uppercase + letter-spacing made this the single widest element on the site
     at very large text (449px at a 64px root), wider than the price tables,
     which fit. Beyond the 200% the SC requires, but a one-word fix. */
  overflow-wrap: anywhere;
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-1);
}

/* Numbered tie-knot instructions */
/* The knot photographs are the smallest files on the site (75x101 to 137x285),
   so they get the densest track, a 16rem column left them filling 12-17%. */
.knot-steps {
  display: grid;
  /* Capped at 5 columns so the 10-step sequence reads as two clean rows of 5
     instead of 6+4. */
  max-width: 56rem;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 10rem), 1fr));
  gap: var(--space-5);
  list-style: none;
  margin: 0;
  padding: 0;
}

.knot-steps > li {
  margin: 0;
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

.knot-steps .card-media { height: 11rem; }

.knot-body { padding: var(--space-4); }

.knot-num {
  display: block;
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-2);
}

.knot-it {
  display: block;
  margin-top: var(--space-2);
  font-style: italic;
  color: var(--text-muted);
  font-size: 0.9375rem;
}

/* --------------------------------------------------------------------------
   11. Tables, real tables, with caption and scoped headers (12)
   -------------------------------------------------------------------------- */

.price-tables {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--space-5) var(--space-6);
  align-items: start;
}

table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.9375rem;
}

caption {
  caption-side: top;
  text-align: left;
  font-family: var(--font-display);
  font-size: 1.1875rem;
  font-weight: 600;
  color: var(--text);
  padding-bottom: var(--space-2);
}

thead th {
  text-align: left;
  font-size: 0.8125rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
  border-bottom: 2px solid var(--border);
  padding: var(--space-2) var(--space-3);
}

thead th:last-child, td:last-child { text-align: right; }

/* Price cells wrap. They previously carried `white-space: nowrap`, which made
   strings like "Ask for price" and "$50.00 and up" incompressible and forced
   the nine price tables to a ~461px min-width at 200% text, the single
   biggest cause of horizontal scrolling on alterations.html. A wrapped price
   is legible; a horizontal scrollbar is a 1.4.10 failure. */

tbody th {
  text-align: left;
  font-weight: 500;
  color: var(--text);
}

tbody th, tbody td {
  padding: var(--space-3);
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}

/* Price tables scroll horizontally rather than shredding their own text.

   History, because the wrong answer here is seductive. Forcing the table to
   fit 320px needs its min-content lowered, and `overflow-wrap: anywhere` does
   that, but it pays for the fit by breaking words. Applied to `td` it split
   prices ("$18.00" over two lines) at DEFAULT text size. Applied to `th` it
   left the service column 116px wide at 200% text, rendering "With lining" as
   "With "/"linin"/"g", 52 row headers at roughly four characters per line.
   The row header is what gives the price its meaning; at three characters wide
   it has stopped being a table.

   WCAG 1.4.10 explicitly permits two-dimensional layout for content that
   requires it and names data tables. So: no word breaking anywhere in the
   table, and each table gets its own scroll region. Measured on alterations
   .html at 320px, mid-token breaks 0 / 0 / 0 at 100 / 200 / 400% text (was
   0 / 52 / 56), and document scrollWidth 320 / 320 / 320 (was 320 / 320 /
   497). Strictly better on every axis; nothing traded.

   The wrapper is a <div>, NOT `display: block` on the <table> itself, that
   would strip the table's semantics from the accessibility tree. */
caption, th, td { overflow-wrap: normal; }

.tbl-scroll {
  overflow-x: auto;
  max-width: 100%;
}

/* min() so the floor can never exceed the region. A bare `18rem` inflates with
   text size: at 200% it forced a 576px table into a 256px window, making the
   ROW-HEADER column (436px) wider than its own scroll window, the reader had
   to scroll sideways to read the label, which is the comprehension failure the
   scroll region was introduced to fix. Measured: 288 / 365 / 729px at
   100 / 200 / 400% (was 288 / 576 / 1152).
   (An earlier comment here claimed the minimum stopped columns collapsing.
   It cannot: with `overflow-wrap: normal` no word can break, so a column can
   never fall below its longest word. The minimum only ever activated above
   100% text, i.e. solely to make the large-text case worse.) */
.tbl-scroll table { min-width: min(18rem, 100%); }

tbody tr:nth-child(odd) { background-color: var(--stripe); }

/* --------------------------------------------------------------------------
   12. Definition lists (contact details, shipping facts)
   -------------------------------------------------------------------------- */

.facts { margin: 0; }

.facts div {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-1);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--rule);
}

@media (min-width: 34rem) {
  .facts div { grid-template-columns: minmax(0, 10rem) minmax(0, 1fr); gap: var(--space-4); }
}

.facts dt {
  font-weight: 700;
  font-size: 0.875rem;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--text-muted);
}

.facts dd { margin: 0; }

/* Stacked links in a definition value were 22px tall, below the 24px floor,
   passing 2.5.8 only via the spacing exception and only by 2.4px. Make them
   meet the floor outright. */
.facts dd a {
  display: inline-block;
  min-height: 24px;
  padding-block: var(--space-1);
}

/* --------------------------------------------------------------------------
   13. Callout
   -------------------------------------------------------------------------- */

.callout {
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius);
  padding: var(--space-5);
  margin-block: var(--space-5);
}

.callout > :last-child { margin-bottom: 0; }
.callout h2, .callout h3, .callout h4 { margin-top: 0; }

/* A callout nested inside a measuring step sits behind two layers of padding
   already (container + .step). At 320px/400% zoom the third layer's own
   padding alone was wider than the space left for it, 196px of padding in a
   63px column, and pushed the page 6px into horizontal scroll (1.4.10).
   Cap it against the viewport: identical to --space-5 at any width >= 480px,
   so nothing above phone width moves. */
.step .callout {
  padding: min(var(--space-5), 5vw);
  min-width: 0;
}

/* --------------------------------------------------------------------------
   14. Form
   -------------------------------------------------------------------------- */

.form { max-width: 38rem; }

.field { margin-bottom: var(--space-5); }

.field label {
  display: block;
  font-weight: 600;
  margin-bottom: var(--space-2);
}

.req {
  font-weight: 400;
  color: var(--text-muted);
  font-size: 0.875rem;
  /* "(required)" is words, not an asterisk and not a colour (3.3.2). */
}

.hint {
  display: block;
  font-weight: 400;
  font-size: 0.875rem;
  color: var(--text-muted);
  margin-top: var(--space-1);
}

.field input,
.field textarea {
  width: 100%;
  min-height: 44px;
  padding: var(--space-3);
  font: inherit;
  color: var(--text);
  background-color: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.field textarea { min-height: 9rem; resize: vertical; }

.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"] {
  /* Invalid state is announced and described in text; the thicker border is
     only a secondary cue, so nothing depends on colour. */
  border-width: 2px;
  border-color: var(--accent);
}

.field-error {
  display: block;
  margin-top: var(--space-2);
  font-weight: 600;
  font-size: 0.9375rem;
  color: var(--accent);
}

.field-error:empty { display: none; }

fieldset {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--space-4);
  margin: 0 0 var(--space-5);
  min-width: 0;
}

/* At very large text the symmetric padding was the dominant contributor to
   about.html's residual overflow (fieldset 373px in a 320px viewport). */
@media (max-width: 30rem) {
  fieldset {
    padding-inline: var(--space-3);
  }

  /* Padding is rem-based, so at very large text it alone can exceed the
     viewport (the submit button reached 251px of which 192px was padding). */
  .btn {
    padding-inline: var(--space-3);
  }

  legend { overflow-wrap: anywhere; }
}

legend {
  padding-inline: var(--space-2);
  font-weight: 700;
}

/* Never `display: none`, the inner `role="alert"` must stay in the
   accessibility tree from page load, or content inserted into it as it becomes
   visible is commonly not announced. So the container is always rendered and
   simply carries no visual weight until JS marks it active. */
.form-status {
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font-weight: 600;
}

.form-status.is-active {
  margin-bottom: var(--space-5);
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-left: 4px solid var(--accent);
  border-radius: var(--radius);
  background-color: var(--surface-2);
}

.form-status ul { margin: var(--space-2) 0 0; }

.form-status h3 {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 700;
}

/* --------------------------------------------------------------------------
   15. Footer
   -------------------------------------------------------------------------- */

.site-footer {
  background-color: var(--footer-bg);
  color: var(--footer-text);
  padding-block: var(--space-7) var(--space-6);
  margin-top: var(--space-8);
}

.site-footer h2 {
  font-size: 1rem;
  font-family: var(--font-body);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--footer-muted);
  margin: 0 0 var(--space-3);
}

.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
  gap: var(--space-6);
}

.site-footer a { color: var(--footer-link); }

.site-footer ul { list-style: none; margin: 0; padding: 0; }

.site-footer li + li { margin-top: var(--space-1); }

/* inline-block, not inline-flex: a flex item establishes a min-content floor
   that an unbreakable email address cannot fall below, which is what pushed
   these to 303-358px at 200% text. inline-block still honours min-height for
   the 24px target floor (2.5.8). */
.site-footer li a,
.footer-contact a {
  display: inline-block;
  min-height: 24px;
  padding-block: var(--space-1);
}

.footer-note {
  margin: var(--space-6) 0 0;
  padding-top: var(--space-4);
  border-top: 1px solid rgb(255 255 255 / 0.34);
  color: var(--footer-muted);
  font-size: 0.875rem;
}

.footer-contact p { margin: 0 0 var(--space-2); }

/* --------------------------------------------------------------------------
   16. Utilities
   -------------------------------------------------------------------------- */

.visually-hidden:not(:focus):not(:active) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.text-muted { color: var(--text-muted); }

/* The owner's price list marks three shortening jobs with an asterisk. We kept
   the mark rather than deleting it or guessing what it means, so it needs to
   read as a footnote reference and not as a required-field marker. */
.price-mark {
  font-weight: 400;
  color: var(--text-muted);
  padding-inline-start: 0.15em;
}

/* A list that reads as prose: used for the mask colour key, which is content
   transcribed out of a photograph rather than a navigational list. */
.plain-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

.plain-list li + li { margin-top: var(--space-1); }

figure { margin: 0; }

figcaption {
  font-size: 0.9375rem;
  color: var(--text-muted);
  margin-top: var(--space-2);
}

/* Shrink-to-fit so a 120x204 photograph is not centred in a 470px frame.
   The figure is still capped to its column and stays centred. */
.feature-figure {
  /* `fit-content` sizes to the widest child, which is usually the caption, so
     a 120px photograph sits in a ~195px frame. That 75px of slack is cosmetic
     and it is the correct trade: `width: min-content` was tried and downscaled
     a 267px photograph to 190px in a 288px column, violating the rule that the
     max-* pair only ever shrinks an image that would otherwise overflow. */
  width: fit-content;
  min-width: 0;
  max-width: 100%;
  margin-inline: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
  background-color: var(--surface);
}

/* `width: 100%` here upscaled a 120x204 photo to 254px wide AND, combined with
   the (previously fabricated) width/height attributes, made the browser
   reserve a wrong-ratio box and then reflow 241px on load. Natural size,
   centred, shrunk only when it would overflow. */
.feature-figure img {
  width: auto;
  height: auto;
  max-width: 100%;
  margin-inline: auto;
}
.feature-figure figcaption { padding: var(--space-3) var(--space-4); margin-top: 0; }

.two-col {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
  gap: var(--space-6);
  align-items: start;
}

/* --------------------------------------------------------------------------
   17. Reduced motion (2.3.3)
   Movement is removed. Opacity and colour transitions survive, because they
   aid comprehension without inducing motion sickness.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  /* Remove MOVEMENT, keep colour. `transition: none` here previously killed the
     buttons' colour fade as well, which is more than reduced-motion asks for:
     the guidance is fewer and gentler animations, not zero. Overriding only
     transition-property keeps the durations and easings already declared. */
  .btn:active,
  .nav-toggle:active,
  .site-nav a:active,
  .header-phone:active {
    transform: none;
  }

  .btn,
  .nav-toggle,
  .site-nav a,
  .header-phone {
    /* All three of property/duration/timing-function must be overridden, or
       the longer base lists leave inert trailing values in the computed style. */
    transition-property: color, background-color, border-color;
    transition-duration: var(--dur-hover);
    transition-timing-function: var(--ease-hover);
  }

  .js .site-nav[data-collapsible],
  .js .site-nav[data-collapsible].is-open {
    transform: none;
    transition: opacity var(--dur-enter) var(--ease-hover), visibility 0s;
  }

  .js .site-nav[data-collapsible] {
    transition: opacity var(--dur-exit) var(--ease-hover),
                visibility 0s linear var(--dur-exit);
  }

}

/* --------------------------------------------------------------------------
   18. Print
   -------------------------------------------------------------------------- */

@media print {
  .site-header, .site-footer, .skip-link, .btn-row { display: none; }
  body { background: #fff; color: #000; }
  .card, .step, table { break-inside: avoid; }
}

/* --------------------------------------------------------------------------
   19. Small screens
   The layout was already fluid; what it lacked was reachability. On a phone
   the two things a visitor needs (the number, and the nav on a 14,000px price
   page) sat 134px tall at the very top and scrolled away permanently.
   -------------------------------------------------------------------------- */

@media (max-width: 47rem) {
  /* One sticky bar carries both. It also removes the need for a back-to-top
     control: Menu is reachable from any scroll depth. */
  /* `[data-sticky]` is set by site.js and only while the bar is a small
     enough share of the viewport to earn permanent space; see the comment on
     initStickyHeader. Without JS the header stays static, the layout the rest
     of this stylesheet was verified against. */
  .site-header[data-sticky] {
    position: sticky;
    top: 0;
    /* Above the page, below the nav panel it owns (z-index: 50). */
    z-index: 40;
  }

  /* The bar is now permanent screen cost, so it pays for its height: the
     wordmark keeps its own row, the two controls share the next one.
     134px -> ~108px, and the 44px targets are untouched. */
  .header-inner {
    gap: var(--space-2) var(--space-3);
    padding-block: var(--space-3);
  }

  /* The number stretches into the space the Menu button leaves, which is the
     larger target and the one worth hitting with a thumb. */
  .header-phone {
    flex: 1 1 auto;
    justify-content: center;
  }

  /* REQUIRED by the sticky header, not cosmetic. Every in-page anchor
     (alterations.html's #price-list and friends) would otherwise land with
     its heading underneath the bar. --space-6 was sized for a static header.

     --header-h is measured and published by site.js, because no authored
     length is correct here: the bar is 122px at 375px wide, taller once the
     wordmark wraps at 320px, and far taller at the 200% text this stylesheet
     is verified against. The fallback is the measured 375px height, so with
     JS off the anchors still clear the bar at the common case, and with JS off
     the header is not sticky anyway. */
  html { scroll-padding-top: calc(var(--header-h, 122px) + var(--space-3)); }

  /* Side-by-side, each button was narrower than its own label wanted and the
     secondary one wrapped to three lines. Full width, stacked, in reading
     order. */
  .btn-row { flex-direction: column; align-items: stretch; }
  .btn-row .btn { justify-content: center; }
}

/* --------------------------------------------------------------------------
   20. Phone portrait: use the width the device actually has

   28rem, not a round number: it is the widest viewport at which `.grid` is
   still ONE column. The grid's track is `minmax(min(100%, 13rem), 1fr)` with
   a --space-5 gap, so a second column appears once the container's content
   box reaches 2 x 13rem + 1.5rem = 440px. With the 12px gutters below, that
   happens at 464px, so everything here is scoped safely inside the
   single-column range. If the grid's track ever changes, re-derive this.
   -------------------------------------------------------------------------- */

@media (max-width: 28rem) {
  /* The fluid gutter was built for the full width range and resolves to
     ~17px on a 390px phone. On a phone that is 8.5% of the screen spent on
     nothing, on both sides. A flat 12px still keeps text off the glass. */
  .container { padding-inline: var(--space-3); }

  /* Same argument one level in: this padding is the card's own inset, and it
     is subtracted from the width an image is allowed to use. */
  .card-body { padding: var(--space-3); }

  /* THE ACTUAL CULPRIT behind images looking small on a phone.
     `.card-media` is a fixed 16rem (256px) box, and the never-upscale rule
     means an image can only ever shrink inside it. On a 4-up desktop grid a
     254px track against a 256px box is proportionate, so portrait photos are
     limited by WIDTH and fill the track. In one 366px-wide phone column the
     same box is far too short, so they are limited by HEIGHT instead and end
     up a narrow strip in a field of grey. Measured at 390px before this rule:
     the suit filled 45% of the card, the key holder 20%, the pair 37%.

     Fixed RATIO rather than fixed height, so the box grows with the column
     instead of ignoring it. Three things this gets right that an
     `height: auto` shrink-wrap did not:

       1. The box has area at first paint, so `loading="lazy"` still fires.
          Shrink-wrapping collapsed every box to 17px and the five lazy card
          images were then NEVER requested, measured: 0 of 6 loaded, only the
          eager hero image appeared in the network log.
       2. The height is known before the image arrives, so there is no
          layout shift as the photos load.
       3. `max-height: 100%` on the image keeps working. Against an
          auto-height parent a percentage height resolves to `auto`, so the
          shrink-wrap silently disabled the existing cap.

     4/5 is tuned to the sources: portrait median 164x244. Landscape photos
     letterbox on the tint, which is what this box already does at every
     other width. Nothing is cropped and nothing is upscaled. */
  .card-media {
    height: auto;
    aspect-ratio: 4 / 5;
    padding: var(--space-2);
  }

  /* `fit-content` sizes this frame to its caption, which on a phone leaves
     the photograph narrower than the screen for no reason. The photo is the
     content here; let the frame take the column and the caption sit under
     the full width of it. */
  .feature-figure { width: auto; }
}
