/* ==========================================================================
   Media Storehouse - "The Gallery" redesign
   templates/assets/msh.css

   Loaded by templates/pageheader.php AFTER css/stylesheet.php, whose
   sitewide rules it deliberately overrides. Everything stays scoped under
   BODY.msh, the class pageheader.php puts on every page.

   Older hooks that must keep working, and are therefore re-skinned here
   rather than replaced: .dmcsHeaderColour, .dmcsMiddle, .dmcsToolbar,
   .dmcsForm, .plain, .glider / .glider-contain (Glider.js filmstrips),
   .skip-main. (The Material Symbols icon-font hook that used to be listed
   here was removed sitewide - see PERF_AUDIT_2026-08-10.md's P2 "web-font
   swap reflow" follow-up in HANDOVER.md - msh_ico() SVGs cover everything
   now.)
   ========================================================================== */

/* --- Tokens -------------------------------------------------------------
   These are DEFAULTS, not the palette. templates/theme.php derives the
   whole set from the current domain's own colours and fonts (the generated
   templates/custom/<host>.css, itself written from the `domain`/`domain_code`
   rows the design admin edits) and templates/pageheader.php re-declares
   them in an inline <style> after this file loads - same `body.msh` selector,
   so it wins on source order. What is left here is what a domain that has
   configured nothing renders as, and the fallback if that override is ever
   skipped. Keep new rules written against the tokens, never against literal
   hex values, or they will not follow a brand's palette.
   ------------------------------------------------------------------------ */
body.msh {
  --paper:        #FAF7F2;
  --paper-2:      #F3EDE4;
  --paper-3:      #EAE1D4;
  --card:         #FFFFFF;

  --ink:          #16130F;
  --ink-2:        #3E362E;
  --ink-3:        #766A5C;   /* 4.5:1 on paper and paper-2 */
  --ink-4:        #938779;   /* decorative only - 3.0:1, never small text */

  --brass:        #B0842F;   /* rules, icons, dots - non-text use */
  --brass-text:   #886624;   /* 4.5:1 accent for text and links */
  --brass-soft:   #E4CE9C;
  --terracotta:   #B2502F;
  --sage:         #5E7358;
  --brand-blue:   #333A83;   /* Media Storehouse wordmark blue */

  /* Hyperlink colour, and the deeper shade it hovers to. Per-domain
     (domain.colour_link -> $dmcsLinkColour -> templates/theme.php), which is
     the whole point of the token: that column has always driven `A, A:visited` in
     css/stylesheet.php, but `body.msh a { color: inherit }` below overrode it, so
     a brand's chosen link colour never reached this design at all.
     Spent on TEXT links only - running prose, .msh-inline-link, .msh-more and
     friends. The toolbar, primary nav, footer, buttons, breadcrumbs and the
     card/tile/panel components deliberately keep their own chrome colours; a
     whole clickable card painted link-colour would stop reading as a card.
     The defaults below are --brass-text and a 30%-towards-black step from it, i.e.
     exactly what those links rendered as before this token existed. */
  --link:         #886624;
  --link-hover:   #5F4719;

  /* Chrome and action tokens. Split out of --ink/--paper (18 Aug 2026) because
     the database already stores a separate colour for each of these roles per
     domain - button fill, button text, header, nav and footer bands - and
     every one of them was previously painted with the palette's generic ink
     and paper, so a branded storefront's own button/header/footer colours
     never reached the redesign at all. templates/theme.php overrides the
     whole set from templates/custom/<host>.css; the values here are the
     defaults the design shipped with, so a domain that has configured nothing
     still renders exactly as before. */
  --btn:          #16130F;
  --btn-text:     #FAF7F2;
  --btn-hover:    #886624;
  --head-bg:      #FAF7F2;
  --head-text:    #16130F;
  --menu-bg:      #FAF7F2;
  --menu-text:    #16130F;
  --menu-text-2:  #3E362E;   /* resting nav link; --menu-text is the hover */
  --foot-bg:      #F3EDE4;
  --foot-text:    #3E362E;

  --line:         #E3D9CA;
  /* was #D2C5B1 - only ever used as a border colour (every text input, panel,
     card and chip on the site), but at 1.59:1 on paper / 1.70:1 on card it
     failed WCAG 1.4.11's 3:1 requirement for a UI-component boundary
     (accessibility review, 12 Aug). Reusing --ink-4's value here: it's
     already the palette's "decorative-only, 3.0:1" tier, which is exactly
     the contrast tier a non-text border needs - so this isn't a new colour,
     just the correct existing token for the job. */
  --line-2:       #938779;

  --sh-1: 0 1px 2px rgba(22,19,15,.04), 0 2px 8px rgba(22,19,15,.04);
  --sh-2: 0 2px 6px rgba(22,19,15,.05), 0 12px 32px rgba(22,19,15,.08);
  --sh-3: 0 6px 16px rgba(22,19,15,.08), 0 32px 64px rgba(22,19,15,.14);
  --sh-frame: 0 2px 4px rgba(22,19,15,.18), 0 18px 44px rgba(22,19,15,.26);

  /* 12 Aug: both tokens now default to the sitewide Roboto stack (matching
     css/stylesheet.php's default) rather than a hardcoded editorial pairing.
     templates/pageheader.php re-declares both to the branded domain's
     own $dmcsFontFamily (domain_code.font_family) in an inline <style>
     block emitted after this file, so this pair is really only the
     fallback for a request where that override didn't run. --serif and
     --sans are two separate custom properties for anything that wants to
     tell headings/body apart in the future, but $dmcsFontFamily is a single
     stack - the override sets both to the same value.
     13 Aug: as of the h1/h2-only display-accent pass below, no rule in this
     file reads --serif any more - h1/h2 use --display, everything else
     uses --sans. --serif is left declared (and still set per-domain by
     pageheader.php) purely so a future rule can opt back into a distinct
     serif token without re-adding it here first. */
  --serif: 'Roboto','Roboto Fallback',Helvetica,Arial,Sans-Serif;
  --sans:  'Roboto','Roboto Fallback',Helvetica,Arial,Sans-Serif;
  /* H1s and H2s get their own font, kept separate from --serif/--sans (the
     per-domain body font) so the two biggest heading levels can carry a
     display face without the rest of the site following. H3 stays on --sans
     (see its own rule below) so the hierarchy doesn't flatten to "everything
     serif".
     18 Aug 2026: this is now per-domain too. templates/theme.php sets it
     from $dmcsTitleFontFamily (domain_code.title_font_name, the "Title Font
     Family" field in the design admin - a genuinely separate setting from the
     main font_family that drives --sans/--serif). The Playfair Display value
     below is only the default for a domain that hasn't set one, and
     pageheader.php only fetches that webfont when it is the one in use. */
  --display: 'Playfair Display', Georgia, 'Times New Roman', serif;

  --ease:     cubic-bezier(.22,.61,.36,1);
  --ease-out: cubic-bezier(.16,1,.3,1);
  --dur: .5s;

  --gutter: clamp(1rem, 4vw, 3.5rem);
  --maxw: 1440px;
  --radius: 3px;

  /* Text-scale multiplier for the header and footer bands, 1 = the sizes this
     design was drawn at. templates/theme.php re-declares it from
     domain.fontsize (the design centre's "Font Size" dropdown) alongside the
     body-copy size it already sets - see msh_font_size() there.

     Why a multiplier rather than letting the chrome inherit body copy: the
     header and footer are built from a dozen deliberately-sized parts (nav
     links, the search field, the basket count bubble, footer link lists) and
     every one of them is a rem off <html>, so nothing inside either band
     inherited the body size at all. Multiplying each of those rem values keeps
     the proportions between them exactly as designed while moving the whole
     band with the dropdown, which plain inheritance could not do.

     calc(<designed rem> * var(--msh-fs)) rather than em throughout: em would
     compound down each nesting level and would re-base every value on whatever
     the band's own size happened to be, so an unconfigured domain would shift.
     At the default 1 every calc below computes to the literal rem it replaced,
     so a domain that has never touched the dropdown renders identically. */
  --msh-fs: 1;
}

/* --- Base --------------------------------------------------------------- */
body.msh {
  margin: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  /* 18 Aug 2026: was a flat 16px. px pinned body copy while every other size in
     this stylesheet is rem off <html> (which nothing sets, so it tracks the
     visitor's browser text size) - so someone who raised their browser text size
     got every component scaled and the actual prose left behind at 16px.
     Confirmed against computed styles on a live PDP, not inferred from this file.
     17px also suits this page's scale better: the PDP h1 computes to 48px and its
     h2 to 40px, so 16px prose underneath them read small.
     This is the DEFAULT body size, not the final word: templates/theme.php
     overrides this same declaration from domain.fontsize (the design centre's
     "Font Size" dropdown) for any domain that has actually set one, via an inline
     <style> emitted after this stylesheet - see msh_font_size() there for why 16
     means "leave this 17px alone". The px->rem swap here did NOT by itself make
     that setting take effect, as an earlier version of this comment claimed:
     css/stylesheet.php applies it as BODY{font-size:Npx}, which loses to this
     body.msh rule on specificity as well as source order. */
  font-size: 1.0625rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body.msh img { max-width: 100%; }

/* 13 Aug: font-family dropped from this shared rule - it was set to --serif
   here but every one of the three tags immediately overrides it with a more
   specific same-specificity rule anyway (h1/h2 to --display below, h3 to
   --sans at its own rule further down), so the value here never actually
   rendered; it just made the base font look like it was --serif at a
   glance. Removed rather than fixed in place, so each heading level's
   real font lives in exactly one rule. */
body.msh h1, body.msh h2, body.msh h3 {
  font-weight: 400;
  line-height: 1.12;
  letter-spacing: -.015em;
  color: var(--ink);
  margin: 0;
}
/* Site-wide h1 spacing pass (11 Aug). The shared h1/h2/h3 rule above zeroes all margin, and
   nothing filled it back in for a *plain* page-title h1 (no special class) - "New Account",
   "Shopping Basket", the homepage's own h1, plenty of others - so those sat completely flush
   against whatever content followed, with zero breathing room. Giving h1 a sensible default
   margin-bottom fixes that everywhere in one place, since it's the majority case; the handful of
   more specific selectors that already set their own margin-bottom (.msh-hero-copy h1 at 1.25rem)
   are more specific selectors and keep winning over this, and any h1 with its own inline
   style='margin...' (a few error banners, one product-title variant) is untouched too - inline
   style beats any stylesheet rule regardless of specificity. Deliberately margin-BOTTOM only, not
   -top: unlike bottom spacing, top spacing is already handled inconsistently per-page (some pages
   put a manual <br> before their h1, others rely on wrapper padding), so a blanket top margin
   risked compounding into double spacing on whichever pages already hand-roll it - that needs a
   page-by-page look, not a one-line CSS fix. One page did have this exact bottom-margin
   collision - pages/login.php's "Returning Customers" heading had a manual trailing <br> for
   spacing that would have doubled up with this; removed there since this rule now does that job. */
/* 12 Aug: max lowered 4rem -> 3rem (and the vw term along with it, so the
   curve between the same 2.25rem floor and the new ceiling is still smooth)
   after removing the product-detail page's own smaller h1 override - a real
   product title ("MOD, Gulfstream Family Poster") at the old 4rem/64px
   wrapped to two lines in that page's narrower media column, dominating the
   page above a comparatively modest product image. clamp() scales off
   VIEWPORT width, not the column's own width, so it can't tell "narrow
   column on a wide screen" from "wide column on a wide screen" - one
   sitewide ceiling has to work for both the full-width homepage hero and
   this narrower two-column layout, which is what led here rather than back
   to a page-specific override. */
body.msh h1 { font-family: var(--display); font-size: clamp(2.25rem, 4vw, 3rem); margin-bottom: 1rem; }
body.msh h2 { font-family: var(--display); font-size: clamp(1.6rem, 3.2vw, 2.5rem); }
/* 12 Aug: h2.msh-pdp-desc-h's own font-size (1.3rem, added 11 Aug to fix this
   description lead-in reading bigger than the product-detail page's own h1)
   removed per request, along with that h1's own font-size override just
   above - both now use their sitewide defaults. Since the sitewide h1 clamp
   is still bigger than the sitewide h2 clamp at every viewport width, the
   hierarchy the 11 Aug fix cared about holds anyway without either
   page-specific override; class='msh-pdp-desc-h' left in the markup, just
   unused by any rule now. */
/* Toolbar-to-h1 spacing pass (11 Aug), follow-up after the pass above. Two bugs found together
   auditing every public page's top spacing (the "page-by-page look" the comment above deferred):
   (1) this shared h1/h2/h3 rule's `margin: 0` (and line-height/letter-spacing) had gone missing -
   confirmed live via the served stylesheet, not just this source file, so it was a real
   regression, not just a stale comment - meaning h1's site-wide had been getting the *browser's*
   default top margin (~0.67em, so it scales with font-size) instead of the deliberate zero this
   comment already claimed. That's an uncontrolled, inconsistent amount, not a designed one.
   (2) with a real zero restored, every page whose h1 is the first thing in <main> (no hero band,
   no breadcrumb, no card wrapper) would have sat with literally NO gap under the sticky nav -
   worse than before. `.msh-page-title` below gives those specific render points (thumb_grid.php's
   shared gallery/search/editors-picks/trending listing heading, plus a handful of standalone page
   h1's - see each PHP file's own comment at the point it's applied) a deliberate top margin,
   instead of leaning on the browser default that happened to produce a passable-by-accident gap.
   Value taken from the prototype (derek1/assets/css/main.css) rather than invented: its .hero
   uses `padding-top: calc(var(--head) + 4.5rem)`, dropping to `calc(var(--head) + 2rem)` at its
   own 860px breakpoint - the var(--head) term compensates for the prototype's nav being
   position:fixed (out of flow, overlaid on content). This site's nav is position:sticky (in
   flow, already pushing content down on its own - see pageheader.php), so only the 4.5rem/2rem
   breathing-room half of that formula applies here; var(--head) would double-count. Pages that
   already get real breathing room from something else (a hero band, a breadcrumb, a card,
   basket/register's own layout) don't get this class - they didn't need it, and stacking it on
   top would just be double spacing again. Specificity note: needs the "body.msh h1" prefix (not
   just ".msh-page-title" alone) to actually beat the shared h1/h2/h3 rule's `margin: 0` above -
   two type selectors (body, h1) plus one class outranks one class alone regardless of source
   order, the same reason .msh-card-h1/.msh-pdp-title below are written the same way. */
body.msh h1.msh-page-title { margin-top: 4rem; }
@media (max-width: 860px) {
  body.msh h1.msh-page-title { margin-top: 2rem; }
}
/* 18 Aug 2026 (gallery top-spacing pass). The paragraph above already states the rule that pages
   opening with a breadcrumb shouldn't take this margin on top - what it couldn't do is drop the
   class on those pages, because thumb_grid.php's h1 is shared by ~9 listing pages and only the
   $GalleryID ones (galleries/collections) render a breadcrumb bar; search, trending, whats-new
   and editors-picks don't. So the rule is expressed as a variant instead: same class for the
   shared styling, half the top margin once a breadcrumb has already opened the page. Measured on
   /everett-collection/television at 1440px, this closed a 64px gap between the breadcrumb and the
   title down to 32px. Same halving at the 860px breakpoint. */
body.msh h1.msh-page-title--crumbed { margin-top: 2rem; }
@media (max-width: 860px) {
  body.msh h1.msh-page-title--crumbed { margin-top: 1rem; }
}
/* The left-hand gallery menu column (index.php's "Left hand gallery menu" block, msh branch
   only). Its top padding does one job: drop the first nav link level with the page h1 beside it.
   That means it has to move whenever that h1's top margin does, so the hardcoded inline
   padding-top:4.5rem it used to carry became wrong the moment --crumbed existed. As a class it
   can track both. Values are the h1's own top margin plus the breadcrumb bar's height (27px)
   less the 17px default <p> margin the first nav link already carries: 2rem + 27 - 17 = 42px
   = 2.625rem for the breadcrumbed case. The non-crumbed 4.5rem is the pre-existing value, kept
   byte-for-byte so trending/whats-new/editors-picks render exactly as before - worth knowing
   that it was only ever tuned for the breadcrumbed layout, so on those three the first link sits
   ~25px below the h1. Left alone deliberately; that's a separate call, not this fix. */
body.msh .msh-gnav-col { padding-top: 4.5rem; }
body.msh .msh-gnav-col--crumbed { padding-top: 2.625rem; }
@media (max-width: 860px) {
  body.msh .msh-gnav-col { padding-top: 2rem; }
  body.msh .msh-gnav-col--crumbed { padding-top: 1.625rem; }
}
/* pages/login.php's sign-in card uses a real <h1> for two of its three heading branches (kept as
   <h1>, not <h2>, so the page still has exactly one true top-level heading for accessibility) -
   but the card itself is a ~300px-wide box, not a full page, so the default h1 clamp above
   (up to 4rem/64px) rendered absurdly oversized inside it. Sized to match the same card's own
   "Checkout as Guest" / "Create Account" h2 headings instead, so both card headings read as the
   same visual weight. Class-only - nothing outside this rule changes. */
/* h2 variant added 19 Aug: on the checkout interstitial the guest card is
   primary and carries the h1, demoting Returning Customers to h2 - same
   size either way, same h1/h2 pairing pattern as .msh-home-lede-title. */
body.msh h1.msh-card-h1,
body.msh h2.msh-card-h1 { font-size: clamp(1.6rem, 3.2vw, 2.5rem); margin: 0; }
/* 18 Aug 2026: the homepage lede heading ("Art Prints, Posters & Puzzles") used to be a second
   <h1>, inline-centred and preceded by a <br>. It is an <h2> now so the page has exactly one
   top-level heading; this keeps the h1 clamp so nothing about how it looks changes, and the
   top margin replaces the <br> that was doing the spacing. Same pattern as .msh-card-h1 above,
   in the other direction. */
body.msh h1.msh-home-lede-title,
body.msh h2.msh-home-lede-title {
  font-size: clamp(2.25rem, 4vw, 3rem);
  text-align: center;
  margin: 1.6rem 0 1rem;
}
/* 12 Aug: h3 used to default to a small sans-serif utility label (.75rem-ish
   feel) and only got the editorial serif treatment via an opt-in
   .msh-sec-h class on specific instances. Per request, that treatment is
   now h3's baseline everywhere - class no longer needed (existing
   class='msh-sec-h' markup still works, just redundant now). A few
   contexts where an h3 sits inline in a flex/grid layout rather than a
   standalone block (the basket drawer header, footer nav columns, a
   product-range card title) override ONLY the margin/border below this
   rule, back to something that doesn't break that layout - see each one's
   own comment. The font itself (family/size/weight/letter-spacing) is the
   same everywhere, no exceptions.
   13 Aug: family switched --serif -> --sans, per request that only h1/h2
   keep the display accent and everything else use the site font. No
   visible change today - pageheader.php's per-domain override
   ($dmcsFontFamily) has always set --serif and --sans to the identical
   stack, so h3 was already rendering as the site font, just under a
   variable named --serif. This is a correctness fix so the two tokens can
   diverge later (a domain wanting a real serif accent on --serif) without
   silently pulling h3 back into serif along with it. */
body.msh h3 {
  font-family: var(--sans);
  font-size: 1.3rem;
  font-weight: 400;
  letter-spacing: -.01em;
  text-transform: none;
  margin: 2.25rem 0 .6rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
}
/* 12 Aug: h1/h2/h3 all get a deliberate reset above (margin, line-height,
   letter-spacing) but h4 never did - the shared selector at the top of this
   section stops at h3. The only h4 anyone had styled was the narrow
   .msh-opt-head case below (a small uppercase label), so any OTHER h4 -
   e.g. thumb_grid.php's "Save to Lightbox" popup heading - fell all the way
   through to the browser's own UA default (bold, ~1.33em top/bottom
   margin), which is exactly the uncontrolled look the h1 spacing-pass
   comment above was written to avoid. Giving h4 the same kind of baseline
   h3 gets (rather than h3's exact values) keeps it one step down in the
   scale should a real h4 heading ever need one. */
body.msh h4 { font-size: 1rem; font-family: var(--sans); font-weight: 600; line-height: 1.3; letter-spacing: 0; color: var(--ink); margin: 0 0 .5rem; }

body.msh a { color: inherit; }
body.msh a.plain, body.msh a.plain:hover { text-decoration: none; }

/* Where --link is spent, and where it deliberately is not.
   The rule directly above is why a per-domain link colour never showed up in this
   design: css/stylesheet.php paints every `A` on the platform with
   $dmcsLinkColour, and `body.msh a { color: inherit }` (0,1,2) beats its
   `A, A:visited` (0,1,1) on every anchor on the page. That rule still has to stand -
   an msh anchor is very often a whole card, tile, panel or nav item whose colour
   belongs to the component, not to the fact that it is clickable - so the domain's
   colour is applied to the anchors that are TEXT LINKS instead, one treatment at a
   time: this one, .msh-inline-link, .msh-cart-empty a and the bare-prose rule at the
   bottom of this file.
   Left out on purpose, all of them chrome or components rather than prose: the
   toolbar (.dmcsToolbar/.dmcsMenuProducts), primary nav (.msh-nav-inner/.msh-actions),
   footer (--foot-text, the band has its own text colour in the database), buttons
   (.msh-btn/.dmcsButton - a fill, not a link), .breadcrumbs, .plain, and the
   picture components (.msh-card, .msh-brands, .msh-fmtpanel, .dmcsThumbGrid*).
   "View all" links carry .msh-more in several places besides .msh-section-head
   (.msh-story, and a couple of bare <p> rows), and only the section-head copy had a
   colour of its own - the rest inherited body ink and did not read as links at all. */
body.msh a.msh-more { color: var(--link); }
body.msh a.msh-more:hover { color: var(--link-hover); }

/* css/stylesheet.php (sitewide legacy CSS, loaded before this file - see this file's own header
   comment) sets `BODY,INPUT,TEXTAREA,BUTTON{font-family:$dmcsFontFamily}` (Roboto/Arial) and
   `INPUT[type=text],INPUT[type=number]...{border:1px solid #888}` for every site on the
   platform, including this one. Rather than edit that shared file (which the print/PDF and
   admin output also depends on), re-skin form controls here the same way `.plain` etc. are
   re-skinned above - scoped entirely to body.msh. */
body.msh input, body.msh textarea, body.msh button, body.msh select {
  font-family: var(--sans);
}
body.msh input[type="text"], body.msh input[type="number"], body.msh input[type="email"],
body.msh input[type="password"], body.msh textarea, body.msh select {
  border-color: var(--line-2);
  color: var(--ink);
}

/* Opt-in polish for a handful of ordinary content forms (currently just the
   My Account "Account Details" form) that want the same rounded/padded
   field look the sign-in/register cards already have, without every plain
   <input> sitewide picking it up. Not applied globally because most
   sitewide forms (contact-us, search filters, etc.) were never audited
   against this look and are out of scope here - see HANDOVER.md "My
   Account: Account Details modernised". PrintEdit() (helper_funcs.php)
   already writes its own inline `style='width:Npx'` on every field it
   echoes, and HTML only honours the FIRST of two same-named attributes -
   a second inline style from a caller would be silently dropped - so this
   has to be a class, passed in via PrintEdit's $CustomEvents param, rather
   than another inline style. */
body.msh .msh-input {
  padding: 10px 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  box-sizing: border-box;
  max-width: 100%;
}
body.msh .msh-input:focus {
  outline: none;
  border-color: var(--ink);
}
body.msh input[type="checkbox"].msh-check {
  width: 16px;
  height: 16px;
  accent-color: var(--ink);
  flex: 0 0 auto;
}

/* Accordion-style section selectors (My Account's My Orders/Basket/Address/
   Account Details/Downloads toggles). The clickable header row itself is
   inline-styled per call site like the rest of my_account.php's msh
   branches - this only covers the bits that need a real CSS rule: a hover
   affordance native to :hover, and the chevron's open/closed rotation
   (toggled by the mshAccToggle() helper in pages/my_account.php, which
   flips this class on the icon rather than swapping an image src the way
   the legacy +/- toggle did). */
body.msh .msh-acc-head:hover { background: var(--paper-2); }
body.msh .msh-acc-chevron { transition: transform .2s var(--ease); }
body.msh .msh-acc-chevron.msh-acc-open { transform: rotate(180deg); }

/* Chrome's own :-webkit-autofill styling (light blue bg + black text, e.g. on the sign-in page's
   remembered email/password fields) is a browser UI layer, not CSS we write - a plain "color"
   rule can't touch it, hence the -webkit-text-fill-color + inset-shadow trick. Real users with a
   saved login for this site hit this, so it's worth keeping on-brand rather than leaving Chrome's
   default blue/black clash against the redesign. The absurd transition delay is the standard
   no-op trick to stop Chrome's own yellow/blue flash from briefly showing before this applies. */
body.msh input:-webkit-autofill,
body.msh input:-webkit-autofill:hover,
body.msh input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0px 1000px var(--card) inset;
  box-shadow: 0 0 0px 1000px var(--card) inset;
  transition: background-color 5000s ease-in-out 0s;
}

/* helper_funcs.php's breadcrumb_trail() (used sitewide, not just here) inline-styles the
   "current" crumb with this site's legacy $SiteTextColor hex, e.g. the "MOD" / "Mary Evans" /
   "Favourites" collection links inside a basket item row. Inline styles beat any CSS selector
   short of !important, so - same reasoning as the input rules above, kept entirely inside this
   msh-only stylesheet rather than editing that shared, sitewide function - override it here. */
body.msh a.plain[style] { color: var(--ink) !important; }

/* css/stylesheet.php's .dmcsToolbar rules (used sitewide for the secondary nav row - e.g. the
   "Collections / Favourites / Mary Evans" links seen on basket/register/my-account) colour
   themselves with this site's legacy $dmcsMenuTextColour hex, same "shared file, don't touch it"
   situation as the input/breadcrumb rules above. Its own :hover already sets
   "color:#fff!important", so overriding that specifically needs !important here too - importance
   beats specificity regardless of source order, so a plain higher-specificity rule wouldn't win. */
body.msh .dmcsToolbar,
body.msh .dmcsToolbar a,
body.msh .dmcsToolbar a:visited { color: var(--ink); }
body.msh .dmcsToolbar:hover,
body.msh .dmcsToolbar a:hover {
  background: var(--ink);
  color: var(--paper) !important;
}

/* ...but inside the primary nav band those same .dmcsToolbar items ARE the site's
   own menu: DrawMenu('H') in helper_funcs.php emits every menu/webpage link as
   <a class='dmcsToolbar'>, and they sit on --menu-bg right next to
   .msh-nav-inner > a links that take their colour from the domain's own
   $dmcsMenuTextColour (templates/custom/<host>.css). Painting them var(--ink)
   instead - the BODY text colour, which has no relationship to the menu band -
   is what made them vanish on any domain whose menu is dark: black on black, or
   #444 ink on arsenalpics' red #f00000 bar. Reported by Derek, 21 Aug 2026.
   Fixed by using the same two menu tokens the neighbouring nav links use
   (theme.php derives both from $dmcsMenuTextColour; --menu-text-2 is the eased
   resting tone, guaranteed >=4.5:1 on the band by msh_readable()), so the whole
   row reads as one menu again.
   !important on the resting colour because css/stylesheet.php's
   ".dmcsToolbar:visited{color:...!important}" would otherwise leave visited items
   a different colour from unvisited ones - importance beats specificity, so a
   plain rule cannot win against it.
   The hover fill changes too: var(--ink) on a coloured menu band looked like a
   hole punched in it. color-mix keeps the chip but derives it from the band's own
   text colour, so it works on a light or a dark menu without a second rule. */
body.msh nav.msh-nav .dmcsToolbar,
body.msh nav.msh-nav a.dmcsToolbar:visited { color: var(--menu-text-2) !important; }
body.msh nav.msh-nav .dmcsToolbar:hover,
body.msh nav.msh-nav a.dmcsToolbar:hover {
  background: color-mix(in srgb, var(--menu-text) 12%, transparent);
  color: var(--menu-text) !important;
}
/* Same band, same bug, different component (reported by Derek 21 Aug 2026 against
   www.fineartstorehouse.com: "fix the login etc buttons on the toolbar to use
   correct colours"). The Basket / Sign Out buttons at the right-hand end of the
   nav row are .msh-btn--ghost, whose base rule is written for the PAGE: text
   var(--ink), hairline var(--line-2) and a var(--paper-2) hover fill, all three
   derived from --paper. Sitting on --menu-bg they are the same dark-on-dark
   disappearance as the nav links above, plus a hover that flashes the page
   colour onto the band. Re-derived here from the band's own text colour, so it
   works on a light or a dark menu without a second rule; color-mix for the
   hairline keeps the 'outline, not fill' look at roughly the weight --line-2
   gives on paper.
   Sign In (.msh-btn--primary) is deliberately NOT touched: it is a solid fill in
   the domain's own $dmcsButtonColour/$dmcsButtonColourtext pair, which is a
   brand decision and already independent of the band. */
body.msh nav.msh-nav .msh-btn--ghost {
  color: var(--menu-text);
  border-color: color-mix(in srgb, var(--menu-text) 45%, transparent);
}
body.msh nav.msh-nav .msh-btn--ghost:hover {
  color: var(--menu-text);
  border-color: var(--menu-text);
  background: color-mix(in srgb, var(--menu-text) 12%, transparent);
}

/* Keyboard equivalent of the mouse-hover reveal above: these toolbar dropdown panels
   (pageheader.php's Categories/Artists/Products mega-menus, and helper_funcs.php's
   dynamic Collections-style menu items) are shown/hidden only via inline
   style="display:none" toggled by onMouseOver/onMouseOut JS, with no keyboard path.
   The panel is a DOM child of its .dmcsToolbar trigger, so :focus-within on the
   trigger reveals it the moment the trigger or any link inside it receives focus,
   and hides it again once focus leaves the whole subtree - no JS needed, and it
   composes correctly with Tab-through-then-away. Same !important requirement as the
   hover rule above: inline display:none only yields to !important. */
body.msh .dmcsToolbar:focus-within > .dmcsMenuProducts {
  display: block !important;
}
/* 18 Aug 2026: the panel's surface and link colour used to be inline-styled #eee/#000
   in helper_funcs.php, which builds this menu for BOTH templates - so one design's
   palette was hardcoded into shared markup, and being inline it beat every rule here.
   The colours were removed from that markup; each design now supplies its own (the
   legacy one via .dmcsMenuProducts in css/stylesheet.php). */
body.msh .dmcsMenuProducts {
  background-color: var(--paper-2);
  color: var(--ink);
  border: 1px solid var(--line);
}
body.msh .dmcsMenuProducts a,
body.msh .dmcsMenuProducts a:visited { color: var(--ink); }
/* 18 Aug 2026 (Derek): this used to be brass-on-panel, but the .dmcsToolbar
   a:hover rule further up also matches links inside the panel and paints the
   hovered row with background:var(--ink) - so the item read as #886624 on the
   dark ink fill (~3.4:1, muddy gold-on-slate). The row highlight is the right
   affordance; the text just has to sit on it, so pair the fill with --paper
   exactly as the toolbar's own hover does. Both declarations are restated here
   rather than left to inherit: the shared css/stylesheet.php rule
   ".dmcsMenuProducts A:hover{background:none}" would otherwise be the only
   background declaration a future reader finds next to this one. */
body.msh .dmcsMenuProducts a:hover {
  background: var(--ink);
  color: var(--paper) !important;
}
/* BUG FIX (12 Aug 2026): the rule above forces display:block, but pageheader.php's
   #dropdowngalleries mega-menu (the "Categories" nav item) is opened by JS with
   display:flex, laying its 6 curated-gallery columns (Arts/Film/Popular/Places/
   Rooms/Artists) out side by side. Clicking any link inside it focuses that link,
   which triggers :focus-within on its .dmcsToolbar ancestor an instant before the
   browser navigates away - and the block !important above was winning over the
   inline flex, collapsing the 6 columns into a single squashed inline text column
   for that last frame before navigation (reported by Derek: "clicking on an item
   the layout changes to a single column"). Override back to flex specifically for
   this menu (ID beats class, so this wins over the block rule regardless of source
   order) - Artists/Products remain simple single-column lists and are unaffected. */
body.msh .dmcsToolbar:focus-within > #dropdowngalleries {
  display: flex !important;
}

/* BUG FIX (20 Aug 2026, reported by Derek: "mobile toolbar selecting Products,
   the drop down menu appears off to the right of the page"): on phones the nav
   row scrolls horizontally (see .msh-nav-inner's overflow-x), and these panels
   are inline-styled "position:absolute" with no left/top - in both
   pageheader.php and helper_funcs.php's DrawMenu(). With no offsets the panel
   resolves against the initial containing block at its STATIC position, i.e.
   wherever the trigger would sit in the row if that row were not scrolled. On
   the portal "Products" is the last toolbar item, so on a narrow screen its
   static position is already past the right edge of the viewport and the panel
   opened out there, off the page; scrolling the nav to reach the trigger made
   it worse, because the panel does not scroll with the row (it is positioned
   against the page, not against the scroller).

   Anchor the panel to the nav bar instead and let it span the full width
   underneath, which is what a dropdown should do at this size anyway. Making
   nav.msh-nav the containing block also keeps the panel exempt from
   .msh-nav-inner's overflow clip - an absolutely positioned box is only clipped
   by a scroll container that also contains its containing block - so this does
   not trade the old bug for a clipped panel. !important throughout because
   every one of these properties is competing with an inline style on the
   markup. Phones only: on desktop the row does not scroll and the panels
   already sit correctly under their own trigger. */
@media (max-width: 760px) {
  body.msh nav.msh-nav { position: relative; }
  body.msh nav.msh-nav .dmcsMenuProducts {
    position: absolute !important;
    top: 100% !important;
    left: var(--gutter) !important;
    right: var(--gutter) !important;
    width: auto !important;
    margin-top: 0 !important;
    padding: .75rem 1rem !important;
    max-height: 70vh;
    overflow-y: auto;
    overscroll-behavior: contain;
  }
  /* #dropdowngalleries is opened with display:flex and lays its curated-gallery
     columns out side by side; across a phone that splinters into unreadable
     slivers, so stack them instead. */
  body.msh nav.msh-nav #dropdowngalleries { flex-wrap: wrap !important; }
  body.msh nav.msh-nav #dropdowngalleries > div {
    flex: 1 0 100%;
    padding: .5rem 0 !important;
  }
}

body.msh .msh-wrap {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

/* No measure cap on body copy. Added 18 Aug 2026, reversed the same day: a 70ch
   cap on paragraphs in .dmcsDivMid was tried for legibility, but on the product
   page it stopped the text at ~666px under a heading that ruled the full 1440px
   column, leaving half the page empty. That reads as broken layout rather than
   as a comfortable measure, and the same mismatch turns up wherever a capped
   paragraph sits under or beside full-width chrome - which on these legacy pages
   is most of them. Per Derek: blocks of text run full width. If a specific block
   ever needs a measure, cap that block, not prose in general. */

/* Lede typography only - no measure cap, per the note above. The 62ch it used
   to carry suited a one-sentence hero lede but left admin-authored copy
   (siteindex.php's "Brands" section, several paragraphs plus a list) sitting
   in a narrow column beside an otherwise full-width section. Where a lede sits
   in a grid cell (the hero, .msh-story) that cell already bounds it. */
body.msh .msh-lede {
  font-size: 1.0625rem;
  color: var(--ink-3);
}

body.msh .msh-eyebrow {
  font-size: .75rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--brass-text);
  font-weight: 600;
}

/* Brand-claim variant: carries _TXT_LARGESTPRINTSTORE ("The Largest Online
   Print Store - over 12 million pictures!") as the hero's opening line -
   promoted 17 Aug 2026 from the small muted .msh-hero-tagline that used to
   sit under the CTAs (that rule is gone). Slightly larger and looser-leaded
   than the base eyebrow so the sentence-length claim reads as a statement
   and wraps cleanly on narrow columns. */
body.msh .msh-eyebrow--brand {
  font-size: .8125rem;
  letter-spacing: .12em;
  line-height: 1.6;
  text-wrap: balance;
}

/* Focus visibility - the legacy sheet suppresses outlines in places. */
body.msh :focus-visible {
  outline: 2px solid var(--brass-text);
  outline-offset: 2px;
  border-radius: var(--radius);
}

body.msh .skip-main {
  position: absolute;
  left: -9999px;
  z-index: 200;
  background: var(--ink);
  color: var(--paper);
  padding: .75rem 1.25rem;
}
body.msh .skip-main:focus { left: 0; top: 0; }

/* --- Icons --------------------------------------------------------------
   Custom inline SVG from templates/icons.php, replacing the Material
   Symbols webfont. They stroke with currentColor, so they take the colour
   of whatever they sit in with no per-icon rules. */
body.msh .msh-icon {
  display: inline-block;
  vertical-align: middle;
  flex: 0 0 auto;
}
/* The -1px optical nudge only belongs where an icon sits on a TEXT baseline.
   Applied globally it also shifted icons inside grid/flex-centred controls -
   the round search button most visibly, where place-items:center had already
   centred it and the nudge then pushed it 1px high. Scoped to inline-with-text
   contexts only. */
body.msh a:not(.msh-btn) > .msh-icon,
body.msh p > .msh-icon,
body.msh b > .msh-icon,
body.msh strong > .msh-icon,
body.msh td > .msh-icon { margin-top: -1px; }
/* Where an icon sits beside text in a flex row it should not stretch. */
body.msh .msh-actions .msh-icon,
body.msh .msh-btn .msh-icon { align-self: center; }

/* --- Header ------------------------------------------------------------- */
body.msh header {
  background: var(--paper);
  border-bottom: 1px solid var(--line);

  /* css/stylesheet.php line 181 has a bare element selector:
       header{padding:10px;cursor:pointer;font-size:larger;font-weight:bold}
     It was plainly written for a collapsible accordion header, but it is
     unscoped, so it also hits the page <header>. Consequences, all of which
     read as "the header is broken" rather than as a stylesheet collision:
       padding:10px      insets the bar, so the background stops 10px short
                         of the viewport edge and the header appears not to
                         span the page
       cursor:pointer    hand cursor over the entire header
       font-size:larger  compounds - it is a relative unit on an ancestor,
                         so every nested element inherits the bump
       font-weight:bold  bolds the whole bar including the search field
     All four reset here. */
  padding: 0;
  margin: 0;
  cursor: auto;
  /* 1rem, not the body's 1.0625rem: the bar is chrome, deliberately a step
     down from running prose. --msh-fs moves it with the domain's font-size
     setting without changing that relationship. */
  font-size: calc(1rem * var(--msh-fs));
  font-weight: 400;
  width: 100%;
}
/* The bar and nav rows stay aligned to _MAX_WIDTH (1440px, same constant the
   rest of the page uses) so the logo lines up with the content beneath it;
   it is the BACKGROUND that now runs edge to edge. */

body.msh .dmcsHeaderColour {
  background: var(--head-bg);
  color: var(--head-text);
}

body.msh .msh-bar {
  display: flex;
  align-items: center;
  gap: clamp(.75rem, 2vw, 2rem);
  max-width: var(--maxw);
  margin-inline: auto;
  padding: .85rem var(--gutter);
}

body.msh .msh-brand { flex: 0 0 auto; display: flex; align-items: center; }
body.msh .msh-brand img {
  display: block;
  height: 44px;
  width: auto;
  /* The only logo on disk for this domain is a JPEG
     (framed-prints-and-photo-gifts.jpg) - the .png in the fallback chain
     does not exist. A JPEG cannot carry transparency, so its white
     background painted a hard white rectangle on the warm --paper header.
     multiply drops white to transparent while leaving the dark wordmark
     intact, which is the closest we get without a real transparent asset.
     Replace with a PNG/SVG and this rule can go. */
  mix-blend-mode: multiply;
}

/* Search - the legacy form is .dmcsForm#searchform */
body.msh .msh-search { flex: 1 1 auto; min-width: 0; }
body.msh form#searchform {
  display: flex;
  align-items: center;
  gap: .5rem;
  width: 100%;
  background: var(--card);
  border: 1px solid var(--line-2);
  border-radius: 999px;
  padding: .35rem .5rem .35rem 1.1rem;
  transition: border-color .2s var(--ease), box-shadow .2s var(--ease);
}
body.msh form#searchform:focus-within {
  border-color: var(--brass);
  box-shadow: 0 0 0 4px rgba(176,132,47,.14);
}
body.msh form#searchform input[type="search"] {
  flex: 1 1 auto;
  min-width: 0;
  width: 100%;
  border: 0;
  outline: 0;
  background: transparent;
  font: inherit;
  font-size: calc(.95rem * var(--msh-fs));
  color: var(--ink);
  padding: .35rem 0;
}
body.msh form#searchform input[type="search"]::placeholder { color: var(--ink-3); }
body.msh form#searchform button[type="submit"] {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  min-width: 40px;
  border: 0;
  border-radius: 999px;
  background: var(--btn);
  color: var(--btn-text);
  cursor: pointer;
  transition: background .2s var(--ease);
  /* Zeroed so the fixed 40x40 box + border-radius:999px stays a true circle -
     any padding here would push the icon off-centre inside it. */
  padding: 0;
}
body.msh form#searchform button[type="submit"]:hover { background: var(--btn-hover); }

/* Header icon cluster (wishlist / basket / account) */
body.msh .msh-actions {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: .25rem;
}
body.msh .msh-actions > a {
  position: relative;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;          /* 44px minimum touch target */
  border-radius: 999px;
  color: var(--ink-2);
  text-decoration: none;
  transition: background .2s var(--ease), color .2s var(--ease);
}
body.msh .msh-actions > a:hover { background: var(--paper-3); color: var(--ink); }
body.msh .msh-count {
  position: absolute;
  top: 4px;
  right: 2px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 999px;
  /* 18 Aug 2026: was terracotta on a hardcoded #fff. The basket count is part
     of the buy flow, so it takes the domain's own action colour rather than a
     fixed palette accent - and its label follows that colour's stored text
     colour instead of assuming white reads on it. */
  background: var(--btn);
  color: var(--btn-text);
  font-size: calc(.6875rem * var(--msh-fs));
  font-weight: 700;
  line-height: 18px;
  text-align: center;
}

/* --- Primary nav -------------------------------------------------------- */
body.msh nav.msh-nav {
  border-top: 1px solid var(--line);
  background: var(--menu-bg);
}
body.msh .msh-nav-inner {
  display: flex;
  align-items: center;
  gap: clamp(.5rem, 2vw, 1.75rem);
  max-width: var(--maxw);
  margin-inline: auto;
  padding: 0 var(--gutter);
  overflow-x: auto;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
}
body.msh .msh-nav-inner::-webkit-scrollbar { display: none; }
body.msh .msh-nav-inner > a {
  flex: 0 0 auto;
  position: relative;
  display: block;
  padding: .85rem .15rem;
  font-size: calc(.9rem * var(--msh-fs));
  font-weight: 500;
  color: var(--menu-text-2);
  text-decoration: none;
  white-space: nowrap;
}
body.msh .msh-nav-inner > a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 100%;
  bottom: 8px;
  height: 1.5px;
  background: var(--brass);
  transition: right .35s var(--ease-out);
}
body.msh .msh-nav-inner > a:hover { color: var(--menu-text); }
body.msh .msh-nav-inner > a:hover::after,
body.msh .msh-nav-inner > a.is-active::after { right: 0; }

/* --- Trust ticker ------------------------------------------------------- */
body.msh .msh-ticker {
  background: var(--ink);
  color: var(--paper);
  font-size: .8125rem;
  overflow: hidden;
  padding: .45rem 0;
}
body.msh .msh-ticker-track {
  display: flex;
  gap: 3rem;
  width: max-content;
  animation: msh-ticker 46s linear infinite;
}
body.msh .msh-ticker:hover .msh-ticker-track { animation-play-state: paused; }
body.msh .msh-ticker span { white-space: nowrap; }
/* No underline on a linked entry: the bar reads as one continuous run of scrolling
   copy, and underlining only the entries that happen to carry a link broke that into
   visibly mismatched fragments. There is no global anchor reset in this sheet, so the
   browser default has to be turned off explicitly. Colour is deliberately left alone:
   body.msh a is color:inherit, which picks up --paper from .msh-ticker above and so
   stays legible whatever a domain has done to its palette. */
body.msh .msh-ticker a { text-decoration: none; }
@keyframes msh-ticker {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* --- Buttons ------------------------------------------------------------ */
/* white-space:nowrap (11 Aug fix): the top-toolbar "Log Out" button wraps to
   two lines ("Log"/"Out") under admin nav (more nav items than a signed-out
   visitor sees leaves less room for the account-buttons cluster) because
   .msh-btn has no nowrap and is a shrinkable flex item - "Basket"/"Subscribe"
   (one word) never showed it, "Log Out" (two words) did. A button's own
   label should never wrap regardless of context, so this is unconditional
   here rather than scoped to one toolbar - matches the precedent already set
   for .msh-buybar .msh-btn below, which is now redundant but left alone. */
body.msh .msh-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  min-height: 46px;
  padding: .7rem 1.5rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  font: inherit;
  font-size: .9375rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: transform .2s var(--ease), background .2s var(--ease), color .2s var(--ease);
}
body.msh .msh-btn:active { transform: translateY(1px); }
body.msh .msh-btn--primary { background: var(--btn); color: var(--btn-text); }
body.msh .msh-btn--primary:hover { background: var(--btn-hover); }
body.msh .msh-btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: var(--line-2);
}
body.msh .msh-btn--ghost:hover { border-color: var(--ink); background: var(--paper-2); }
body.msh .msh-btn--block { width: 100%; }

/* --- Sections ----------------------------------------------------------- */
body.msh .msh-section { padding-block: clamp(2.5rem, 6vw, 5rem); }
body.msh .msh-section-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-bottom: clamp(1.25rem, 3vw, 2.25rem);
}
body.msh .msh-section-head h2 { margin: 0; }
body.msh .msh-section-head .msh-more {
  font-size: .875rem;
  font-weight: 600;
  color: var(--link);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
  white-space: nowrap;
}

/* Standalone heading rows - sections whose original markup emitted a bare
   <h2> with no wrapper to close, so no wrapper was introduced. */
body.msh .msh-section-head--solo { margin-top: clamp(2.5rem, 6vw, 5rem); }

/* Filmstrip cards need an explicit width: the Glider track lays children out
   in a row, so the grid rules above do not apply inside one. */
body.msh .glider .msh-card { width: 300px; padding-inline: 6px; }
body.msh .glider .msh-card-media { aspect-ratio: 1 / 1; }

/* --- Hero --------------------------------------------------------------- */
body.msh .msh-hero {
  position: relative;
  display: grid;
  grid-template-columns: 1.05fr .95fr;
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: center;
  padding-block: clamp(2rem, 5vw, 4.5rem);
}
/* Hero-scale display type (13 Aug): the sitewide h1 clamp tops out at 3rem,
   which reads underweight beside a ~600px mosaic once the band runs
   full-bleed. Homepage/jigsaw hero h1 only. text-wrap:balance stops a long
   gallery description leaving a one-word second line. */
body.msh .msh-hero-copy h1 {
  margin-bottom: 1.25rem;
  font-size: clamp(2.6rem, 3.4vw, 3.6rem);
  text-wrap: balance;
}
body.msh .msh-hero-copy .msh-lede { margin-bottom: 1.75rem; font-size: 1.125rem; }
body.msh .msh-hero-cta { display: flex; gap: .75rem; flex-wrap: wrap; }
/* Static brand-scale line under the hero CTAs - deliberately quieter than the
   .msh-lede AI subtitle above it (smaller size, muted color, no bottom margin
   of its own) so it doesn't compete with the primary headline/CTA hierarchy. */
/* Hero band. A warm tinted field behind the hero separates it from the plain
   sections below - that opening "cover" feel is what the prototype has and a
   page starting straight into content does not. */
body.msh .msh-hero-band {
  position: relative;
  /* Full-bleed (13 Aug): the band used to be boxed inside .dmcsMiddle's
     1440px column, so on monitors wider than that it floated as a tinted
     rectangle with hard vertical edges. The "cover" feel described above
     wants edge-to-edge. Negative margins pull the band out to the viewport;
     <main>'s overflow-x:clip (pageheader.php) swallows the half-scrollbar
     overshoot, so no horizontal scrollbar appears. Content position is
     unchanged: the inner .msh-wrap caps at the same 1440px .dmcsMiddle
     already did. Also applies to jigsaw-puzzles.php's band - same markup. */
  margin-inline: calc(50% - 50vw);
  background:
    radial-gradient(120% 90% at 82% 12%, rgba(176,132,47,.13) 0%, transparent 58%),
    linear-gradient(180deg, var(--paper-2) 0%, var(--paper) 100%);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}
/* Faint paper tooth over the band - pure CSS, no image request. */
body.msh .msh-hero-band::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: .5;
  background-image:
    repeating-linear-gradient(0deg, rgba(22,19,15,.018) 0 1px, transparent 1px 3px),
    repeating-linear-gradient(90deg, rgba(22,19,15,.018) 0 1px, transparent 1px 3px);
}
body.msh .msh-hero-band > .msh-hero { position: relative; }

/* --- Hero mosaic --------------------------------------------------------
   Four square crops in an offset grid, so the hero reads as an archive
   rather than one hero shot. Falls back to a single image when the trending
   table has fewer than four rows for this region. */
body.msh .msh-mosaic {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-auto-rows: 1fr;
  gap: clamp(.5rem, 1.2vw, .9rem);
  aspect-ratio: 1 / 1;
}
body.msh .msh-mo {
  display: block;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--paper-3);
  box-shadow: var(--sh-2);
}
body.msh .msh-mo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .8s var(--ease-out);
}
/* Offsetting two tiles stops it reading as a contact sheet. */
body.msh .msh-mo-1 { transform: translateY(-6%); }
body.msh .msh-mo-4 { transform: translateY(6%); }
body.msh .msh-mo-solo { grid-column: 1 / -1; grid-row: 1 / -1; transform: none; }
@media (hover: hover) and (pointer: fine) {
  body.msh .msh-mo:hover img { transform: scale(1.06); }
}
@media (max-width: 1024px) {
  /* Stacked under the copy, a square mosaic is far too tall. */
  body.msh .msh-mosaic { aspect-ratio: 16 / 9; }
  body.msh .msh-mo-1, body.msh .msh-mo-4 { transform: none; }
}
@media (max-width: 600px) {
  body.msh .msh-mosaic { aspect-ratio: 4 / 3; gap: .45rem; }
}

/* --- Hero entrance -------------------------------------------------------
   CSS-only (no [data-reveal]): the hero is above the fold, so it should move
   the moment it paints rather than wait for the IntersectionObserver in
   msh.js - and `both` fill means a browser that ignores @keyframes simply
   shows the finished state. The first mosaic tile is deliberately NOT
   animated: it is the page's LCP element (see the fetchpriority note in
   siteindex.php), and starting it at opacity 0 would push the measured LCP
   to the end of the fade. Tiles 2-4 fade opacity only, because mo-1/mo-4
   carry permanent translateY offsets a transform animation would clobber.
   The IsMobile hero branch is also left alone: its prints_puzzles.jpg is
   that layout's LCP. Reduced motion: the global block below zeroes
   animation duration AND delay, so none of this holds content invisible. */
@keyframes msh-rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}
@keyframes msh-fade {
  from { opacity: 0; }
  to   { opacity: 1; }
}
body.msh .msh-hero-copy > * { animation: msh-rise .55s var(--ease-out) both; }
body.msh .msh-hero-copy > *:nth-child(2) { animation-delay: .07s; }
body.msh .msh-hero-copy > *:nth-child(3) { animation-delay: .14s; }
body.msh .msh-hero-copy > *:nth-child(4) { animation-delay: .21s; }
body.msh .msh-hero-copy > *:nth-child(5) { animation-delay: .28s; }
body.msh .msh-hero-copy > *:nth-child(6) { animation-delay: .35s; }
body.msh .msh-mo-2 { animation: msh-fade .6s var(--ease) .15s both; }
body.msh .msh-mo-3 { animation: msh-fade .6s var(--ease) .3s both; }
body.msh .msh-mo-4 { animation: msh-fade .6s var(--ease) .45s both; }

/* Mobile-branch gallery-of-the-day hero (siteindex.php's IsMobile path).
   Was a hardcoded teal (#b0d0ce) inline-styled box left over from the
   pre-msh design; restyled (13 Aug) into the same warm band language as
   the desktop hero above. Same markup, query and click behaviour - only
   inline styles swapped for these classes. */
body.msh .msh-hero-mobile {
  padding: 1.25rem 1.25rem 1.5rem;
  background:
    radial-gradient(120% 90% at 82% 12%, rgba(176,132,47,.13) 0%, transparent 58%),
    linear-gradient(180deg, var(--paper-2) 0%, var(--paper) 100%);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
}
body.msh .msh-hero-mobile .msh-eyebrow { margin-bottom: .4rem; }
body.msh .msh-hero-mobile h2 { margin-bottom: .75rem; }
body.msh .msh-hero-mobile img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--radius);
}
body.msh .msh-hero-mobile .msh-lede { margin-block: .75rem; }
/* The localized "Explore featured <gallery> Buy Prints" label can run long
   on a narrow phone - let this one button wrap rather than overflow. */
body.msh .msh-hero-mobile .msh-btn {
  margin-top: .25rem;
  white-space: normal;
  text-align: center;
}

body.msh .msh-card {
  display: block;
  text-decoration: none;
  color: inherit;
}
body.msh .msh-card-media {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  background: var(--paper-3);
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--sh-1);
  transition: box-shadow .35s var(--ease), transform .35s var(--ease);
}
body.msh .msh-card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .7s var(--ease-out);
}
body.msh .msh-card-title {
  display: block;
  margin-top: .7rem;
  font-size: .9rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink);
}
body.msh .msh-card-meta {
  display: block;
  margin-top: .15rem;
  font-size: .8125rem;
  color: var(--ink-3);
}

/* Hover effects only where hovering is actually possible. On touch the
   card is already fully visible, so nothing is hidden behind a hover. */
@media (hover: hover) and (pointer: fine) {
  body.msh .msh-card:hover .msh-card-media {
    box-shadow: var(--sh-2);
    transform: translateY(-3px);
  }
  body.msh .msh-card:hover .msh-card-media img { transform: scale(1.045); }
}
@media (hover: hover) and (pointer: fine) {
}

/* --- Glider filmstrips (legacy component, re-skinned) ------------------- */
/* glider.css sets `.glider-contain { width: fit-content }`, which sizes the
   box to its content's max-content width (the full unscrolled filmstrip),
   capped only by max-width: 1440px above - never by the actual viewport. On
   narrow screens that left the filmstrip's own container up to 1440px wide,
   spilling far past the right edge of the page. Overriding width to fill
   the available space (still capped at 1440px via max-width) fixes it. */
/* BUG FIX (13 Aug 2026): glider.js also inline-styles every .glider-contain
   with "float:left" (needed for ITS OWN width:fit-content shrink-wrap trick,
   which the width:100% override above already makes irrelevant). Left
   floated, the container stopped contributing its real height to the page's
   normal flow: whatever block follows it (the next section's <br> and h3,
   e.g. "Gallery Wall Inspiration" right after the "You May Also Like" strip)
   rendered starting from where the float BEGINS rather than where it visibly
   ends, so the heading's sitewide border-top divider line painted underneath
   the still-extending filmstrip images instead of below them (reported by
   Derek: "You May Also Like images are overwriting the horizontal line above
   Gallery Wall Inspiration"). Confirmed live: the h3 that should follow sat
   ~273px higher than the filmstrip's actual bottom edge, and its own height
   ballooned to ~296px as its text line-wrapped around the float instead of
   clearing it. Inline styles beat plain CSS regardless of selector
   specificity, so cancelling float here needs !important. */
body.msh .glider-contain { position: relative; width: 100%; float: none !important; }
body.msh .glider { scrollbar-width: none; }
body.msh .glider::-webkit-scrollbar { display: none; }
body.msh .glider-prev,
body.msh .glider-next {
  position: absolute;
  top: 42%;
  z-index: 5;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  box-shadow: var(--sh-1);
  transition: background .2s var(--ease), color .2s var(--ease);
}
body.msh .glider-prev:hover,
body.msh .glider-next:hover { background: var(--ink); color: var(--paper); }
body.msh .glider-prev { left: -6px; }
body.msh .glider-next { right: -6px; }
body.msh .glider-prev.disabled,
body.msh .glider-next.disabled { opacity: .3; pointer-events: none; }

/* --- Catalogue grid (thumb_grid.php / PrintThumbGrid) -------------------
   This is the single shared grid behind gallery, search, collection,
   editors-picks, trending, browse-category, favourites and lightbox pages.
   It lives in /www/dmcs2/thumb_grid.php, which is include()d by absolute
   path, so there is no template-directory override hook for it.

   Rather than fork a 3,100-line shared file that every one of those pages
   depends on, the grid is re-skinned here in CSS only. Zero PHP risk: if the
   redesign is switched off, none of this loads and the grid is untouched.

   !important is load-bearing below, and is the correct tool here rather than
   sloppiness: PrintThumbGrid emits its frame and shadow treatment as INLINE
   style attributes built in PHP (style='$frame$BoxShadow'), and an inline
   style beats any external selector regardless of specificity. Overriding a
   inline style you do not control is precisely the case !important exists
   for. Each override below is narrow and commented with what it undoes.
   ------------------------------------------------------------------------ */

/* Cells flow as inline-blocks, so an even card grid can be built without
   needing a selector for the container (which PrintThumbGrid does not
   consistently emit). */
body.msh div.dmcsThumbGrid,
body.msh div.dmcsThumbGridGallery,
body.msh div.dmcsThumbGridSearch {
  display: inline-block;
  vertical-align: top;
  width: 250px;
  max-width: 100%;
  padding: 0 !important;      /* undoes DIV.dmcsThumbGrid*{padding:15px 0} */
  margin: 0 10px 2rem;
  text-align: left;
}

/* ASPECT RATIO. thumb_grid.php serves two different sources and they must not
   be treated alike:
     gallery tiles  -> /sq/  a square crop made server-side
     media tiles    -> /t/   the picture at its NATURAL ratio
   Forcing 1:1 + object-fit:cover on both (which the first pass did) crops every
   artwork in the browse grid to a square - a landscape painting loses its ends
   and a portrait loses its top and bottom. On a picture library that is a
   content bug, not a styling choice. Square is kept only where the source is
   already square. The prototype does the same thing via its `natural` option. */
body.msh div.dmcsThumbGrid img,
body.msh div.dmcsThumbGridSearch img {
  aspect-ratio: auto;
  object-fit: contain;
  /* Cap the tallest portraits so one 2:3 image cannot set the row height. */
  max-height: 300px !important;
}


/* HOVER MOTION. The rule this replaces hovered div.dmcsThumbGrid*:hover img -
   dead on arrival: those classed divs are the CAPTION under the picture (see
   thumb_grid.php ~line 2290) and contain no img, so it never matched anything.
   The cell wrapper itself carries no class (an inline-styled <div id="m<id>">,
   or <div id="item<N>"> when appended by infinite scroll), so the same
   grid-positional hook the quick-view reveal further down relies on is used.

   Two treatments, because the grid serves two kinds of picture:
   - framed-print thumbs carry an inline 3D tilt (thumb_grid.php's $BoxShadow:
     perspective(1000px) rotateY(10deg) scale(.9)) - on hover the print
     straightens to face the viewer. The hover transform restates the FULL
     perspective stack rather than just rotateY, because transform is one
     property: a bare translateY here would replace the tilt in a single jump.
   - square /sq/ collection tiles (img.dmcsThumbGridCell, plus the .grow class
     api/ajax.php puts on its scrolled-in copies) get a small lift and grow.
     These selectors outrank the legacy sheet's IMG.grow:hover{scale(1.1)
     !important} on specificity, so msh gets one consistent, gentler grow.
   !important: both end states override inline style attributes. Verified on
   derek 20 Aug by injection before landing. */
body.msh [id^="dmcsgrid"] img[style*="perspective"],
body.msh [id^="dmcsgrid"] img.dmcsThumbGridCell,
body.msh [id^="dmcsgrid"] img.grow {
  transition: transform .45s var(--ease-out), box-shadow .45s var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  body.msh [id^="dmcsgrid"] > div:hover img[style*="perspective"] {
    transform: perspective(1000px) translate3d(0, 0, 90px) rotateY(0deg) rotateX(0deg) scale(.95, .95) !important;
    box-shadow: 6px 14px 28px 0 #999 !important;
  }
  body.msh [id^="dmcsgrid"] > div:hover img.dmcsThumbGridCell,
  body.msh [id^="dmcsgrid"] > div:hover img.grow {
    transform: translateY(-4px) scale(1.03) !important;
    box-shadow: 0 10px 24px rgba(26, 23, 20, .16) !important;
  }
}

/* Title and price links below each thumbnail. */
body.msh a.dmcsThumbGrid,
body.msh a.dmcsThumbGridGallery,
body.msh a.dmcsThumbGridSearch {
  display: block;
  margin-top: .7rem;
  font-size: .9rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink);
  text-decoration: none;
  transition: color .2s var(--ease);
}
body.msh a.dmcsThumbGrid:hover,
body.msh a.dmcsThumbGridGallery:hover,
body.msh a.dmcsThumbGridSearch:hover { color: var(--brass-text); }

/* The "from £x framed" line is emitted with an inline style='color:#c00',
   a red that fails contrast on paper and reads as an error rather than a
   price. */
body.msh div.dmcsThumbGrid a[style*="c00"],
body.msh div.dmcsThumbGridGallery a[style*="c00"],
body.msh div.dmcsThumbGridSearch a[style*="c00"] {
  color: var(--ink) !important;
  font-weight: 600;
}

@media (max-width: 760px) {
  body.msh div.dmcsThumbGrid,
  body.msh div.dmcsThumbGridGallery,
  body.msh div.dmcsThumbGridSearch {
    width: 44%;
    margin: 0 2% 1.5rem;
  }
}

/* --- Curated-listing header + card polish (20 Aug 2026) ------------------
   pages/editors-picks.php, pages/trending.php, pages/whatsnew.php and
   pages/imagesoftheday.php all render through PrintThumbGrid (whatsnew.php
   echoes the same h1.msh-page-title + h2 pair itself before calling it), so,
   per the catalogue-grid section's rule above, they are restyled here in CSS
   only - no PHP fork, the curation queries and CheckMediaAccess filtering
   are untouched. Scoped to pageheader.php's body[data-page] hook so the
   other listing pages (gallery, search, collection...) render exactly as
   before. :is() keeps the list readable; a pre-:is() browser drops the rule
   and simply gets the previous look.

   What it fixes: these pages' H2 straplines are emitted bare, so they took
   the sitewide h2 rule's clamp(1.6rem,3.2vw,2.5rem) display face and
   rendered as a second title nearly the size of the h1; and the
   _TXT_EDITORSPICKS2 intro paragraph (editors-picks/imagesoftheday) ran the
   full 1350px content width. Neither element carries a class, hence the
   sibling selectors - the h1 + h2 (+ p where a caption is passed) run is
   what these pages always emit, with no breadcrumb ahead of it ($GalleryID
   is 0 on all four). Where no caption p directly follows the h2 (trending's
   grid, whats-new's blog carousel <div>) the p rule matches nothing. On
   pn>1 crawler pages the caption p is skipped; same story. */
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) h1.msh-page-title {
  text-wrap: balance;
}
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) h1.msh-page-title::after {
  content: "";
  display: block;
  width: 56px;
  height: 3px;
  margin: 1.1rem auto 0;
  border-radius: 2px;
  background: var(--brass);
}
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) h1.msh-page-title + h2 {
  font-family: var(--sans);
  font-size: 1.125rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: .01em;
  color: var(--ink-2);
  max-width: 46rem;
  margin: 1rem auto 0;
  text-wrap: balance;
}
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) h1.msh-page-title + h2 + p {
  max-width: 62ch;
  margin: .8rem auto 2.4rem;
  font-size: .95rem;
  line-height: 1.65;
  color: var(--ink-3);
}

/* Card captions: PrintThumbGrid separates the title link and the price link
   (both display:block under the a.dmcsThumbGrid* rule above) with a <br>,
   which renders as a full blank line inside every card. Hiding it closes the
   ~26px hole between title and price; the gallery cells' "<br>N Items" text
   still starts on its own line because the block-level title link precedes
   it. Scoped to these four pages only - the shared grid keeps its spacing
   elsewhere. */
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) [id^="dmcsgrid"] div.dmcsThumbGrid br,
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) [id^="dmcsgrid"] div.dmcsThumbGridGallery br {
  display: none;
}
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) [id^="dmcsgrid"] div.dmcsThumbGrid a[style*="c00"],
body.msh:is([data-page="editors-picks"],[data-page="trending"],[data-page="whats-new"],[data-page="imagesoftheday"]) [id^="dmcsgrid"] div.dmcsThumbGridGallery a[style*="c00"] {
  margin-top: .3rem;
  font-size: .9rem;
}

/* --- Product / media detail page (pages/media_detail.php) ----------------
   Same reasoning as the catalogue grid: media_detail.php is 423KB of shared
   buying-flow code reached through pages/, and forking it to change colours
   and spacing would be a poor trade. Re-skinned here against the stable
   class hooks it already emits (.productarea, .dmcsButton, .breadcrumbs,
   .product-border-right). No PHP touched, so the add-to-basket path,
   pricing and product option logic are all bit-for-bit unchanged.
   ------------------------------------------------------------------------ */

/* Product page layout. The markup for .msh-pdp / .msh-pdp-media /
   .msh-pdp-buy comes from pages/media_detail.php, emitted only when
   the visitor is not on the mobile chrome.

   The original was a flex-wrap with min-width:300px on both columns, which
   made the buy panel drop below the image at widths where both fitted
   comfortably. A grid gives the two columns a real ratio and one explicit
   breakpoint instead. */
body.msh .msh-pdp {
  display: grid;
  grid-template-columns: minmax(0, 1.45fr) minmax(320px, 420px);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: start;
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
body.msh .msh-pdp-media { min-width: 0; overflow: hidden; }
body.msh .msh-pdp-media.product-border-right { border-right: 0 !important; }

/* The buy panel follows you down the page. Long product pages meant
   scrolling back up to reach the format picker and the add-to-basket button. */
body.msh .msh-pdp-buy {
  position: sticky;
  top: 1.5rem;
  min-width: 0;
  padding: clamp(1rem, 2vw, 1.5rem);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

/* Was margin-bottom: .5rem, on the stated reasoning that "the price/buy button sits right under
   it" (see the h1 spacing pass comment above). Checked that against the actual layout (11 Aug,
   spacing tidy-up pass) and it doesn't hold: this h1 is emitted once, inside .msh-pdp-media (the
   left/image column of the .msh-pdp grid - see media_detail.php ~line 3208), directly followed by
   the product image itself. The buy panel (.msh-pdp-buy, with the price/Add to Basket button) is
   the grid's separate second column, not "right under" this heading at any breakpoint, including
   the <1000px stacked layout (columns stack in DOM order: media column - image and all - first,
   buy panel after). With nothing actually needing the tightness, .5rem read as cramped - a ~38px
   serif title sitting 8px above a large hero image with no breathing room. Dropped the override
   to fall back on the sitewide h1 default (1rem) instead of picking a new bespoke value. */
/* 12 Aug: font-size override removed per request - this h1 now uses the
   sitewide h1 clamp (2.25rem-4rem) like every other h1, rather than its own
   smaller one. (The remaining properties here still just duplicate the
   sitewide h1 rule, except line-height: 1.15 vs. the sitewide 1.12 -
   left alone since nothing here asked for it and it's a real, if tiny,
   intentional difference, not obviously dead weight.) */
body.msh h1.msh-pdp-title {
  font-family: var(--display);
  font-weight: 400;
  line-height: 1.15;
  letter-spacing: -.015em;
}

@media (max-width: 1000px) {
  body.msh .msh-pdp { grid-template-columns: 1fr; }
  /* Sticky is wrong once the panel is full width and stacked - it would
     cover the image on the way past. */
  body.msh .msh-pdp-buy { position: static; }
}

/* Was: max-height:calc(100vh - 3rem); overflow-y:auto here, so a sticky panel
   taller than the viewport could still be scrolled to reach the add-to-basket
   button instead of stranding it off-screen. Removed on request (11 Aug) -
   it was also the thing turning any bit of extra content (the admin-only
   lightbox picker in particular) into a visible internal scrollbar the
   moment the panel's natural height exceeded the viewport. Trade-off now
   accepted: on a short window with enough content to make the panel taller
   than the viewport, the sticky panel simply stops sticking once its own
   bottom edge is reached (normal position:sticky behaviour), and reaching
   the buy button may need scrolling the page rather than the panel. */

/* --- Format picker ------------------------------------------------------
   Each option is its own <form> that submits on click (that is how the
   legacy page switches product type). Only the presentation changes here -
   the forms, hidden inputs and submit behaviour are untouched. */
/* Option group, matching the prototype: hairline rule above, a small
   uppercase label on the left and the current selection on the right. */
body.msh .msh-opt {
  padding: 1.3rem 0;
  border-top: 1px solid var(--line);
}
body.msh .msh-opt-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: .5rem 1rem;
  flex-wrap: wrap;
  margin-bottom: .9rem;
}
/* .68rem (11px) was too small for real reading, not just a decorative label -
   this is the "PRODUCTS · N" heading above the whole format picker. Chrome's
   own "minimum font size" accessibility setting was masking this in-session
   (silently floors every font-size to 16px, so it never showed up in a live
   computed-style sweep here) - bumped to .75rem (12px) against the raw CSS
   value, not the sandbox's floored rendering. Still clearly styled as a
   small caps label (letter-spacing + uppercase + muted colour), just legible
   at real size now. (.msh-pdp-buy h3 below used to get this same small-label
   treatment too, before h3 became a single universal look sitewide.) */
body.msh .msh-opt-head h4 {
  margin: 0;
  font-family: var(--sans);
  font-size: .75rem;
  font-weight: 600;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-3);
}
body.msh .msh-opt-head b {
  font-size: .82rem;
  font-weight: 500;
  text-align: right;
  color: var(--ink);
}

/* The legacy buy column labels its option groups with plain <h3> - font now
   comes entirely from the sitewide h3 rule above (was its own small
   uppercase label style before h3 became a single universal look). Kept
   only this panel's own tighter divider rhythm (was already smaller than
   the sitewide h3's, which suits this denser column better than swapping
   in the bigger editorial spacing would). */
/* The >h2 is the panel's lead heading ("Framed Prints From ..."), demoted-in-
   size to match: it was an h3 until the 18 Aug heading-order fix (h1->h3 skip)
   retagged it. The extra font resets pull it out of the sitewide h2 display
   treatment so nothing changes visually. */
body.msh .msh-pdp-buy > h2,
body.msh .msh-pdp-buy h3 {
  font-family: var(--sans);
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0;
  margin: 1.1rem 0 .7rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--line);
  /* 13 Aug: sized down from the sitewide 1.3rem serif and recoloured. At
     full h3 scale "Framed Prints From Media Storehouse" outshouted the
     price and competed with the page's real h1; the panel is a dense
     control column, not editorial copy. Family left alone (h3 resolves to
     the sans stack sitewide - see the --display token comment near the top
     of this file); deliberately NOT the grey caps-label treatment, which
     would blur into the "PRODUCTS · N" .msh-opt-head idiom directly above.
     --brass-text is the 4.5:1 text accent, so it reads as a warm section
     marker rather than another control. */
  font-size: 1.05rem;
  color: var(--brass-text);
}
/* Checkbox/radio labels inside those groups are not section headings. */
body.msh .msh-pdp-buy h3 label {
  font-size: .875rem;
  letter-spacing: 0;
  text-transform: none;
  font-weight: 500;
  color: var(--ink);
}

body.msh .msh-fmt-wrap { margin-bottom: 1rem; }
/* 18 Aug: since the reorder this strip's heading is a full sentence ("This
   artwork is also available as" - 40 chars in German), not the two-word
   "PRODUCTS - N" label the tracked-caps .msh-opt-head idiom was built for.
   Sentence case at body-adjacent size instead; still muted so it reads as
   helper text introducing the tiles, not another section heading. */
body.msh .msh-fmt-wrap .msh-opt-head h4 {
  text-transform: none;
  letter-spacing: 0;
  font-size: .875rem;
  font-weight: 500;
}
/* 13 Aug: the picker used to be stacked icon-over-label tiles on a
   minmax(96px,1fr) grid - 118px squares, ~96px tall, 3 to a row in the
   420px buy panel. Ten product groups made 4 rows (~400px) and pushed the
   price and Add to Basket button to the bottom edge of a 1080p viewport
   (below the fold on laptops). Rebuilt as horizontal icon+label rows on a
   minmax(148px,1fr) auto-fill grid: 2 columns in the sidebar (~5 rows of
   44px = ~250px, a ~150px saving), and on the <=1000px stacked layout the
   full-width panel gets 3-5 columns for free, shrinking it further. Rows
   beat further-shrunken stacked tiles because the labels stay one line,
   left-aligned and legible at full size, and the 44px min-height keeps the
   whole row a comfortable touch target - the icons stay at their designed
   26px (icons.php draws them to be distinguishable at 26px, no smaller). */
body.msh .msh-fmt-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
  gap: .4rem .5rem;
  user-select: none;
}
body.msh .msh-fmt {
  display: block;
  min-width: 0;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--card);
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
body.msh .msh-fmt form {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .4rem .6rem;
  cursor: pointer;
  min-height: 44px;          /* comfortable touch target */
}
body.msh .msh-fmt:hover { border-color: var(--ink-3); background: var(--paper-2); }
/* Selected state was previously a 1px colour change plus a grey shadow,
   which was very hard to pick out among eight tiles. */
body.msh .msh-fmt.is-on {
  border-color: var(--ink);
  background: var(--paper-2);
  box-shadow: inset 0 0 0 1px var(--ink);
}
body.msh .msh-fmt-ico {
  display: grid;
  place-items: center;
  min-height: 26px;
  flex: 0 0 26px;           /* icon column: label wraps, icon never squashes */
  /* The SVGs stroke with currentColor, so setting colour here is what makes
     them respond to the tile's state - that is the whole reason for using
     inline SVG over the icon font. */
  color: var(--ink-3);
  transition: color .2s var(--ease);
}
body.msh .msh-fmt-ico img, body.msh .msh-fmt-ico svg { max-height: 26px; width: auto; }
body.msh .msh-fmt:hover .msh-fmt-ico { color: var(--ink-2); }
body.msh .msh-fmt.is-on .msh-fmt-ico { color: var(--ink); }
body.msh .msh-fmt-label {
  font-size: .75rem;
  line-height: 1.25;
  text-align: left;
  min-width: 0;             /* let long labels ("Licensed Download") wrap
                               inside the grid cell instead of blowing it out */
  color: var(--ink-2);
}
body.msh .msh-fmt.is-on .msh-fmt-label { color: var(--ink); font-weight: 600; }

/* 12 Aug: trust-badge row (Fast Delivery/Great Value/100% Payment Secure/30 Days Return, pages/
   media_detail.php) - each badge's margin/width/text-align was switched on $_SESSION['IsMobile'].
   One class, one media query at the same max-width:600px breakpoint as .msh-info-box above. */
body.msh .msh-trust-badge { margin: 6px; }
@media (max-width: 600px) {
  body.msh .msh-trust-badge { margin: 10px; width: 90%; text-align: center; }
}

/* 12 Aug: shipping-region info box (pages/media_detail.php) - was two separate PHP-echoed
   <div> variants switched on $_SESSION['IsMobile'] (a card treatment on desktop, a bare div on
   mobile). Same box, one class, and a max-width:600px media query drops the card treatment on
   narrow viewports instead of a server-side device guess. */
body.msh .msh-info-box {
  /* 18 Aug 2026: was #fafafa / #ddd / 10px / #000, carried over verbatim from the legacy
     markup. Four off-system values in one rule: a cool grey panel on a warm paper palette, a
     1.5:1 border that fails the same WCAG 1.4.11 3:1 boundary rule --line-2 exists to satisfy,
     a radius belonging to no scale on the site, and pure black where the palette ink is
     #16130F. Same box, same shape, tokens instead of literals. */
  background-color: var(--paper-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  color: var(--ink);
  padding: 10px 15px 0 15px;
}
@media (max-width: 600px) {
  body.msh .msh-info-box {
    background-color: transparent;
    border: none;
    border-radius: 0;
    color: inherit;
    padding: 0;
  }
}

/* The fixed-width desktop buy panel is ~12px too narrow for its longest size
   label ("Modern Frame 14"x12" (36x31cm) (Popular)") at inherited 16px, so
   the select's displayed value clipped mid-word ("(Popula"). Slightly
   smaller type, desktop only: on phones the full-width panel already fits
   the label, and form fields under 16px trigger iOS's focus auto-zoom. */
@media (min-width: 601px) {
  body.msh .msh-pdp-buy select { font-size: .9rem; }
}

/* The buy panel's lead heading ("Framed Prints From ..."). Since the 18 Aug
   reorder it is the FIRST element in the panel, so the sitewide h3 look -
   top border + padding, meant to divide sections mid-page - read as a stray
   bar floating above the panel. Neutralised here, same treatment as
   .msh-foot-grid h3. */
body.msh .msh-pdp-buy > h2:first-child,
body.msh .msh-pdp-buy > h3:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: none;
}

/* Pins the buy box's stars-link jump: the review widgets above #reviewsio
   lazy-load and grow after the anchor navigation, shoving the target down.
   :target-only so the reserved space never shows as a dead block for media
   whose product-review widget hides itself (hide_if_no_results). */
body.msh #reviewsio:target {
  min-height: 380px;
  scroll-margin-top: 16px;
}

/* Compact trust badges inside the buy box (media_detail.php), directly under
   the Add to Basket cluster. When this renders, the full-width mid-page
   badge row is suppressed server-side so the badges appear once per page. */
body.msh .msh-trust-mini {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .5rem .75rem;
  margin: .85rem 0 .35rem;
  font-size: calc(.8rem * var(--msh-fs));
  font-weight: 600;
  color: var(--ink-2);
}
body.msh .msh-trust-mini span {
  display: flex;
  align-items: center;
  gap: .4rem;
  white-space: nowrap;
}

/* --- Price + primary action -------------------------------------------- */
body.msh .msh-price {
  display: flex;
  align-items: baseline;
  gap: .6rem;
  margin: 1.25rem 0 .85rem;
}
body.msh .msh-price-now {
  /* 13 Aug: --serif -> --sans, same pass as body.msh h3 above - h1/h2 are
     the only things that keep the display accent now. No visible change
     (see that rule's comment for why). */
  font-family: var(--sans);
  font-size: 2rem;
  line-height: 1;
  color: var(--ink);
}
body.msh .msh-price-was {
  font-size: .95rem;
  color: var(--ink-3);
  text-decoration-thickness: 1px;
}

/* Static Reviews.io aggregate under the price (media_detail.php, portal only),
   linking down to the on-page review widget. The stars are two stacked copies
   of the same five glyphs: an --ink-4 base with a --brass overlay clipped to
   the rating percentage - no images, no JS. */
body.msh a.msh-rating {
  display: flex;
  align-items: baseline;
  gap: .5rem;
  margin: 0 0 .85rem;
  text-decoration: none;
}
body.msh .msh-stars {
  position: relative;
  display: inline-block;
  font-size: 1rem;
  line-height: 1;
  color: var(--ink-4);
}
body.msh .msh-stars-fill {
  position: absolute;
  left: 0;
  top: 0;
  overflow: hidden;
  white-space: nowrap;
  color: var(--brass);
}
body.msh .msh-rating-text {
  font-size: .875rem;
  color: var(--ink-3);
}
body.msh a.msh-rating:hover .msh-rating-text {
  color: var(--brass-text);
  text-decoration: underline;
}
/* Homepage hero variant of the stars line (siteindex.php). */
body.msh .msh-rating--hero { margin: .2rem 0 1.1rem; }
/* The hero stars sweep in left-to-right once the copy has settled. The
   inline width on .msh-stars-fill is the real rating, so the sweep animates
   max-width over it and the fill stops exactly where the rating says
   (overflow is already hidden). Hero only - buy-box stars stay static. */
@keyframes msh-stars-sweep {
  from { max-width: 0; }
  to   { max-width: 100%; }
}
body.msh .msh-rating--hero .msh-stars-fill { animation: msh-stars-sweep .8s var(--ease-out) .55s both; }

/* Example-search chips under the hero CTAs (siteindex.php): pill links to
   evergreen searches, teaching what the 12M-image catalogue holds. */
body.msh .msh-hero-chips {
  margin: .9rem 0 0;
  font-size: calc(.85rem * var(--msh-fs));
  color: var(--ink-3);
}
body.msh .msh-hero-chips a {
  display: inline-block;
  margin: 0 .3rem .4rem 0;
  padding: .25rem .7rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  color: var(--ink-2);
  text-decoration: none;
}
body.msh .msh-hero-chips a:hover {
  border-color: var(--brass);
  color: var(--brass-text);
}

/* One real customer quote about THIS picture, below the buy box's trust
   badges (media_detail.php; only renders when the image has a suitable
   4+ star review of its own). */
body.msh .msh-review-quote {
  margin: .85rem 0 0;
  font-size: calc(.85rem * var(--msh-fs));
  color: var(--ink-2);
}
body.msh .msh-review-quote blockquote { margin: 0; font-style: italic; }
body.msh .msh-review-quote figcaption { margin-top: .25rem; color: var(--ink-3); }

/* The action goes full width. Styling hangs off this wrapper rather than the
   Button() helper, so the helper and its dmcsButton class are untouched. */
body.msh p.msh-buy { margin: 0 0 .6rem; }
body.msh p.msh-buy .dmcsButton,
body.msh p.msh-buy input,
body.msh p.msh-buy button { width: 100%; }

/* --- Sticky buy bar -----------------------------------------------------
   Built by msh.js once the real add-to-basket scrolls out of view. Hidden
   by default so it never appears without JavaScript, where its button
   could not work. */
body.msh .msh-buybar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;
  background: var(--card);
  border-top: 1px solid var(--line);
  box-shadow: 0 -6px 24px rgba(22,19,15,.10);
  transform: translateY(100%);
  transition: transform .32s var(--ease-out);
  /* Sits above the iOS home indicator rather than under it. */
  padding-bottom: env(safe-area-inset-bottom, 0);
}
body.msh .msh-buybar.is-on { transform: none; }
body.msh .msh-buybar-in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  max-width: var(--maxw);
  margin-inline: auto;
  padding: .7rem var(--gutter);
}
body.msh .msh-buybar-info { min-width: 0; display: flex; flex-direction: column; }
body.msh .msh-buybar-info b {
  font-weight: 500;
  font-size: .875rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
body.msh .msh-buybar-info span {
  font-family: var(--sans); /* 13 Aug: --serif -> --sans, see .msh-price-now comment above */
  font-size: 1.15rem;
  color: var(--ink);
}
body.msh .msh-buybar .msh-btn { flex: 0 0 auto; white-space: nowrap; }

/* The bar repeats the buy box's stars line (cloned in by msh.js's buybar()),
   tightened to sit under the bar's price without growing the bar. Sizes and
   colors are restated because the .msh-buybar-info span rule above outranks
   the base .msh-stars/.msh-stars-fill/.msh-rating-text rules inside the bar.
   .msh-stars-fill needs the same font-size restated as .msh-stars, not just
   its color - the gold overlay glyphs are absolutely positioned on top of
   the grey base glyphs, so a leftover 1.15rem from the span rule (while the
   base sits at .8rem) desynced the two, reading as two mismatched sets of
   stars instead of one overlaid set. */
body.msh .msh-buybar .msh-rating { margin: 0; gap: .35rem; }
body.msh .msh-buybar .msh-rating .msh-stars { font-size: .8rem; color: var(--ink-4); }
body.msh .msh-buybar .msh-rating .msh-stars-fill { font-size: .8rem; color: var(--brass); }
body.msh .msh-buybar .msh-rating .msh-rating-text { font-size: .75rem; color: var(--ink-3); }

@media (max-width: 600px) {
  body.msh .msh-buybar-info b { display: none; }   /* the title is right above it */
  body.msh .msh-buybar .msh-btn { flex: 1 1 auto; }
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-buybar { transition: none; }
}

/* --- Editorial blocks below the fold ------------------------------------ */
/* h3.msh-sec-h used to be defined here (font-family/size/weight/letter-spacing/
   margin/padding/border-top) as an opt-in "editorial section heading" look.
   Removed 12 Aug - those are now h3's sitewide default (see the shared h3
   rule near the top of this file), so the rule here was pure duplication.
   Existing class='msh-sec-h' markup elsewhere is harmless, just redundant. */
/* 12 Aug: h3.msh-sec-h only ever marks the product page's own below-h2
   headings (product-type name, "Features In These Collections", "Visual
   Description" - see pages/media_detail.php). The shared h3 rule's default
   top margin is 2.25rem/36px; every
   OTHER below-the-fold section heading on this same page (Gallery Wall,
   Discover Collections, Trending Items, Similar, Recently Viewed, You May
   Also Like) sets an explicit 60px top margin inline, so these three sat
   noticeably closer to their preceding content than the rest - bumped to
   match per request. */
body.msh h3.msh-sec-h { margin-top: 60px; }
body.msh p.msh-feature {
  display: inline-block;
  margin: 0 .4rem .4rem 0;
  padding: .3rem .7rem;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--paper-2);
  font-size: .8125rem;
}
body.msh p.msh-feature a { color: var(--ink-2); text-decoration: none; }
body.msh p.msh-feature:hover { border-color: var(--ink-3); }

/* The product page's inline-styled thumbnails (the "You May Also Like"/
   Discover Collections/Trending/Similar/Recently Viewed filmstrip slides and
   the Gallery Wall tiles - see pages/media_detail.php's $_MSHRELCLS). Same
   hover lift as .msh-card-media, minus the image zoom:
   these wrappers have no overflow clipping, so a scaled image would spill
   onto its neighbours. Lift only, on hover-capable devices. */
body.msh .msh-rel-card { transition: transform .35s var(--ease); }
@media (hover: hover) and (pointer: fine) {
  body.msh .msh-rel-card:hover { transform: translateY(-3px); }
}

body.msh .breadcrumbs,
body.msh a.breadcrumbs {
  color: var(--ink-3);
  font-size: .8125rem;
  letter-spacing: .01em;
}
body.msh a.breadcrumbs:hover { color: var(--ink); }

/* The product option panels. */
body.msh .productarea {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: clamp(1rem, 2.5vw, 1.75rem);
}
body.msh .product-border-right { border-right: 1px solid var(--line) !important; }

/* Primary action. The legacy button is a mid-blue with a hard 3px offset
   drop shadow and a contrast(150%) hover filter, which on the brand blue
   produces a noticeably different hue rather than a darker shade. */
body.msh .dmcsButton,
body.msh a.dmcsButton,
body.msh input.dmcsButton,
body.msh button.dmcsButton {
  display: inline-flex !important;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: .75rem 1.75rem !important;
  border: 1px solid transparent !important;
  border-radius: var(--radius) !important;
  background: var(--btn) !important;
  color: var(--btn-text) !important;
  font-family: var(--sans);
  font-size: .9375rem !important;
  font-weight: 600 !important;
  line-height: 1.2 !important;
  box-shadow: none !important;
  text-decoration: none;
  transition: background .2s var(--ease), transform .2s var(--ease);
}
body.msh .dmcsButton:hover,
body.msh a.dmcsButton:hover,
body.msh input.dmcsButton:hover,
body.msh button.dmcsButton:hover {
  background: var(--btn-hover) !important;
  filter: none !important;          /* undoes contrast(150%) hue shift */
  box-shadow: none !important;
}
body.msh .dmcsButton:active,
body.msh a.dmcsButton:active { transform: translateY(1px); }

/* Sysadmin: compact buttons. Added 18 Aug 2026 on request.
   The sitewide .dmcsButton above is sized for a storefront - 48px min-height and
   a 1.75rem horizontal pad give it a comfortable touch target next to a product.
   The sysadmin screens are the opposite case: dense data tables and long edit
   forms where Button() is emitted 189 times across pages/sys*.php, often several
   to a row, and at storefront size those rows carry more button than content.
   Scoped by the data-page attribute that pageheader.php already emits on <body>
   (`data-page='sysusers'` etc.), so this reaches every pages/sys*.php screen and
   nothing on the public site. Height/type-scale only - colour, radius, hover and
   focus all still come from the rule above, so these stay recognisably the same
   button rather than becoming a second style to maintain.
   .dmcsButtonIcon (BulletButton()'s wordless arrow submit, 99 call sites in this
   area) only overrides horizontal padding in css/stylesheet.php, so it inherits
   the smaller height from here and stays aligned with its text-button siblings
   instead of standing 14px taller in the same row. */
body.msh[data-page^="sys"] .dmcsButton,
body.msh[data-page^="sys"] a.dmcsButton,
body.msh[data-page^="sys"] input.dmcsButton,
body.msh[data-page^="sys"] button.dmcsButton,
body.msh[data-page^="sys"] .dmcsButtonGrey,
body.msh[data-page^="sys"] .dmcsButtonUnselected {
  min-height: 34px !important;
  padding: .3rem 1rem !important;
  font-size: .8125rem !important;
}

/* Unselected / secondary option buttons read as outline chips. */
body.msh .dmcsButtonUnselected,
body.msh .dmcsButtonGrey {
  min-height: 48px;
  padding: .75rem 1.5rem !important;
  border: 1px solid var(--line-2) !important;
  border-radius: var(--radius) !important;
  background: var(--card) !important;
  color: var(--ink) !important;
  font-weight: 500 !important;
  box-shadow: none !important;
}
body.msh .dmcsButtonUnselected:hover,
body.msh .dmcsButtonGrey:hover {
  border-color: var(--ink) !important;
  background: var(--paper-2) !important;
}

/* --- Product range page (pages/products.php) -------------------- */
/* Licensing caveat under the range lede, and under the portal homepage's range
   panels. Deliberately quiet - it qualifies the list, it doesn't sell anything. */
body.msh .msh-rangenote {
  margin: .6rem 0 0;
  max-width: 90ch;
  font-size: .875rem;
  color: var(--ink-3);
}
body.msh .msh-range-count {
  margin: 2.5rem 0 1rem;
  font-size: .8125rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-3);
}
body.msh .msh-range {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 1px;
  background: var(--line);
  border-block: 1px solid var(--line);
}
body.msh .msh-range-item {
  display: flex;
  gap: 1.25rem;
  padding: clamp(1.25rem, 3vw, 2rem);
  background: var(--paper);
}
body.msh .msh-range-n {
  flex: 0 0 auto;
  font-family: var(--sans); /* 13 Aug: --serif -> --sans, see .msh-price-now comment */
  font-size: 1.05rem;
  /* was var(--brass), the palette's "rules, icons, dots - non-text use" token (~3.2:1) - this is
     a text numeral, so it needs --brass-text, the 4.5:1 sibling meant for exactly this
     (accessibility review: confirmed live via axe-core color-contrast on jigsaw-puzzles.html). */
  color: var(--brass-text);
  padding-top: .15rem;
}
/* This is an <h3.msh-range-title> sitting as a card title (see .msh-range-item
   above), not a standalone section heading - h3's sitewide margin/padding/
   border-top (a divider meant for content that follows other content) would
   push the title down inside the card and draw a stray line above it.
   Neutralised back to a plain card-title margin; font itself is untouched,
   still the sitewide h3 look. */
body.msh .msh-range-title { margin: 0 0 .5rem; padding-top: 0; border-top: none; }
body.msh .msh-range-desc { font-size: .9375rem; color: var(--ink-3); margin: 0; }

/* --- Product range panels (homepage) ------------------------------------ */
body.msh .msh-fmtpanels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 1px;
  margin-top: 1.75rem;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
body.msh .msh-fmtpanel {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  padding: clamp(1.1rem, 2.5vw, 1.6rem);
  background: var(--card);
  text-decoration: none;
  color: inherit;
  transition: background .25s var(--ease);
}
body.msh .msh-fmtpanel b {
  font-family: var(--sans); /* 13 Aug: --serif -> --sans, see .msh-price-now comment */
  font-size: 1.2rem;
  font-weight: 400;
}
body.msh .msh-fmtpanel span {
  font-size: .875rem;
  color: var(--ink-3);
  /* Descriptions vary from a phrase to a paragraph; clamping keeps the grid
     from developing one very tall row. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
body.msh .msh-fmtpanel:hover { background: var(--paper-2); }
body.msh .msh-fmtpanel:hover b { color: var(--brass-text); }

/* "From" price on a product-range panel. Pinned to the card's bottom edge with
   margin-top:auto (same trick as .msh-store-url below) so the prices line up
   across a row whose descriptions clamp to different heights, and set in the
   sans stack a shade under the panel title - a figure to scan, not a headline
   competing with the product name above it. The display/clamp resets undo the
   3-line clamp .msh-fmtpanel span puts on the description this shares a tag
   with, and the selector repeats ".msh-fmtpanel span" because that description
   rule is (0,2,2): a plain "body.msh .msh-fmtprice" loses to it and the price
   silently inherits the clamp and the description's size/colour. */
body.msh .msh-fmtpanel span.msh-fmtprice {
  margin-top: auto;
  padding-top: .6rem;
  display: block;
  -webkit-line-clamp: unset;
  overflow: visible;
  font-family: var(--sans);
  font-size: .9375rem;
  color: var(--ink-2);
}
body.msh .msh-fmtpanel:hover span.msh-fmtprice { color: var(--brass-text); }

/* --- Other print stores (homepage) ---------------------------------------
   Same panel grid as the product range above (.msh-fmtpanels), but these
   cards link OUT to partner stores on their own domains. Two differences:
   the description clamp is lifted (the charity note is the point of the
   copy, so it must not be the line that gets cut), and each card ends with
   the bare domain so the outbound destination is visible before clicking. */
body.msh .msh-otherstores .msh-fmtpanel span {
  display: block;
  -webkit-line-clamp: unset;
  overflow: visible;
}
body.msh .msh-otherstores .msh-store-url {
  margin-top: auto; /* pins the domain to the card's bottom edge */
  padding-top: .6rem;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--link);
}

/* --- "How Media Storehouse works" --------------------------------------- */
body.msh .msh-story {
  margin-top: clamp(2.5rem, 6vw, 5rem);
  padding-block: clamp(2.5rem, 6vw, 4.5rem);
  background: var(--paper-2);
  border-block: 1px solid var(--line);
}
body.msh .msh-story-inner {
  display: grid;
  grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr);
  gap: clamp(1.5rem, 4vw, 4rem);
  align-items: center;
}
body.msh .msh-story-media {
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--sh-2);
  /* The source is a wide production photo; a portrait crop sits better
     beside a column of text at desktop widths. */
  aspect-ratio: 4 / 3;
}
body.msh .msh-story-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
body.msh .msh-story h2 { margin-top: .5rem; }
body.msh .msh-story .msh-lede { margin-top: 1rem; }
body.msh .msh-story .msh-more { display: inline-block; margin-top: 1.5rem; }

@media (max-width: 860px) {
  body.msh .msh-story-inner { grid-template-columns: 1fr; }
  body.msh .msh-story-media { aspect-ratio: 16 / 9; }
}

/* --- Newsletter band ---------------------------------------------------- */
body.msh .msh-signup {
  margin-top: clamp(2.5rem, 6vw, 5rem);
  padding-block: clamp(2.25rem, 5vw, 3.5rem);
  background: var(--ink);
  color: var(--paper);
}
body.msh .msh-signup-inner {
  display: grid;
  grid-template-columns: 1fr minmax(280px, 460px);
  gap: clamp(1.25rem, 4vw, 3rem);
  align-items: center;
}
body.msh .msh-signup h2 { color: var(--paper); }
body.msh .msh-signup p { margin-top: .5rem; color: rgba(255,255,255,.66); font-size: .8125rem; }
body.msh .msh-signup .msh-subscribe { margin: 0; }
body.msh .msh-signup .msh-subscribe input[type="email"] {
  background: rgba(255,255,255,.06);
  border-color: rgba(255,255,255,.28);
  color: var(--paper);
}
body.msh .msh-signup .msh-subscribe input[type="email"]::placeholder { color: rgba(255,255,255,.5); }
/* On the dark band the primary button would be ink-on-ink. */
body.msh .msh-signup .msh-btn--primary { background: var(--paper); color: var(--ink); }
body.msh .msh-signup .msh-btn--primary:hover { background: var(--brass-soft); }

@media (max-width: 860px) {
  body.msh .msh-signup-inner { grid-template-columns: 1fr; }
}

/* --- Gallery quick view -------------------------------------------------
   Button injected into each grid cell by msh.js. The cell is position:static
   in the legacy sheet, so it needs a containing block for the overlay. */
/* The containing block is set on the wrapper by msh.js (it is an id-based
   element with inline styles, so there is no reliable selector for it). */

body.msh .msh-quick {
  position: absolute;
  left: 50%;
  bottom: 10px;          /* inside the foot of the image */
  transform: translate(-50%, 6px);
  z-index: 4;
  padding: .5rem 1rem;
  min-height: 38px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: .8125rem;
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  box-shadow: var(--sh-2);
  transition: opacity .22s var(--ease), transform .22s var(--ease-out);
}
body.msh .msh-quick:hover { background: var(--ink); color: var(--paper); }

/* Hover-reveal only where hovering exists. */
@media (hover: hover) and (pointer: fine) {
  /* Descendant, NOT a child combinator: the button is appended to the image
     box inside the wrapper, so "> .msh-quick" would never match.
     Two wrapper shapes: thumb_grid.php's server-rendered cells are
     <div id="m<MediaID>">, but the cells its infinite scroll appends are
     <div id="item<N>"> (api/ajax.php's markup carries no m-id since the
     20 Aug restructure), so the grid-positional selector is what reaches
     those; the m-id one stays for grids rendered outside a #dmcsgrid. */
  body.msh [id^="dmcsgrid"] > div:hover .msh-quick,
  body.msh div[id^="m"]:hover .msh-quick,
  body.msh .msh-quick:focus-visible {
    opacity: 1;
    pointer-events: auto;
    transform: translate(-50%, 0);
  }
}
/* On touch there is no hover, so a hidden control is simply unreachable -
   show it permanently rather than making the feature invisible on phones. */
@media (hover: none), (pointer: coarse) {
  body.msh .msh-quick {
    opacity: 1;
    pointer-events: auto;
    transform: translate(-50%, 0);
    min-height: 44px;
  }
}

/* --- Quick view modal --------------------------------------------------- */
body.msh .msh-qv {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  /* The single column MUST be minmax(0,1fr), not the implicit auto track.
     An auto track sizes to its content's max-content width, and the box's
     own "max-width: min(880px, 100%)" then resolves that 100% against the
     track it is itself inflating - so a wide child (the collection grid's
     three columns) pushes the track past the viewport and the modal renders
     wider than the screen. Measured at a 390px viewport: a 460px box.
     Bounding the track to the container breaks the loop for both variants. */
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  padding: clamp(1rem, 4vw, 3rem);
  background: rgba(18,15,12,.72);
  animation: msh-qv-in .2s var(--ease-out);
}
@keyframes msh-qv-in { from { opacity: 0; } to { opacity: 1; } }
body.msh .msh-qv-box {
  position: relative;
  max-width: min(880px, 100%);
  max-height: 100%;
  display: flex;
  flex-direction: column;
  gap: .9rem;
  background: var(--card);
  border-radius: var(--radius);
  padding: clamp(.9rem, 2vw, 1.4rem);
  box-shadow: var(--sh-3);
}
/* Child combinator, not descendant: the single-picture modal appends its <img>
   straight to the box, but the collection modal nests thumbnails inside
   .msh-qv-grid, and "contain at natural width" is the opposite of what those
   want. Same specificity as the grid's own img rule, so a descendant selector
   here would leave them settling it on source order. */
body.msh .msh-qv-box > img {
  min-height: 0;
  /* vh is the LARGE viewport (as if the address bar were hidden), so on a
     real phone with the address bar showing it overshoots the actual visible
     height - dvh tracks what is on screen right now. Declared second so
     browsers without dvh support silently keep the vh fallback above. */
  max-height: 70vh;
  max-height: 70dvh;
  width: auto;
  margin-inline: auto;
  object-fit: contain;
}
body.msh .msh-qv-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}
body.msh .msh-qv-foot b { font-weight: 500; font-size: .95rem; }
body.msh .msh-qv-count { font-size: .9rem; color: var(--ink-2); }

/* --- Collection quick view ----------------------------------------------
   Same modal chrome, different body: a grid of the pictures INSIDE the
   collection (api/gallery_preview.php) rather than a lightbox of the cover
   image the visitor is already pointing at. */
body.msh .msh-qv-box--set { max-width: min(720px, 100%); }
body.msh .msh-qv-title {
  font-weight: 500;
  font-size: 1.05rem;
  padding-right: 2rem;   /* clear of the close button on narrow screens */
}
body.msh .msh-qv-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: .6rem;
  /* The picture rows scroll, not the whole modal - the title and the CTA stay
     put on a short viewport. */
  overflow-y: auto;
  min-height: 0;
  /* Same vh-vs-dvh gap as the single-image modal above - on a phone with the
     address bar on screen, a 60vh grid was tall enough to slice through the
     bottom row (images cut off mid-cell, caption missing) rather than
     settling on a size that fits the row it can actually show. dvh sizes
     against the real visible viewport instead of the large one. */
  max-height: 60vh;
  max-height: 60dvh;
  /* A short phone still can't fit every row, so the box legitimately needs to
     scroll - the bug was that a cut-off row read as broken layout rather than
     "more below". The classic two-gradient scroll-shadow: a fade painted at
     the visible edges (background-attachment:scroll, so it moves with the
     content and is covered by opaque background once that edge is fully
     scrolled past) plus an opaque cover the same colour as the card
     (background-attachment:local, so it scrolls WITH the content and only
     ever sits over the one row-edge closest to each end). Net effect: the
     fade only shows while there is more grid to reveal in that direction, and
     disappears on its own at each scroll extreme - no JS scroll listener
     needed, and background-attachment (unlike mask-image) needs no -webkit-
     prefix on the Safari this bug was reported on. */
  background:
    linear-gradient(var(--card) 30%, rgba(255,255,255,0)) center top,
    linear-gradient(rgba(255,255,255,0), var(--card) 70%) center bottom,
    radial-gradient(farthest-side at 50% 0, rgba(18,15,12,.18), rgba(18,15,12,0)) center top,
    radial-gradient(farthest-side at 50% 100%, rgba(18,15,12,.18), rgba(18,15,12,0)) center bottom;
  background-repeat: no-repeat;
  background-size: 100% 24px, 100% 24px, 100% 10px, 100% 10px;
  background-attachment: local, local, scroll, scroll;
}
/* Three columns suits a full result (the endpoint asks for six), but a small
   collection - or one where per-media access rules trimmed the six - lands on
   a count that leaves a lopsided hole: four pictures render 3 + 1. Only the
   counts that actually look wrong are overridden. No conflict with the narrow
   -screen rule below despite winning on specificity: every count it names
   resolves to two columns or fewer there anyway. */
body.msh .msh-qv-grid[data-n="1"] { grid-template-columns: 1fr; }
body.msh .msh-qv-grid[data-n="2"],
body.msh .msh-qv-grid[data-n="4"] { grid-template-columns: repeat(2, 1fr); }

body.msh .msh-qv-cell {
  display: block;
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-3);
}
/* Hub fallback cells link to a subgallery, not a picture - the name is
   printed over the cover so it reads as a collection, not a stray photo. */
body.msh .msh-qv-cell-label {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 1.4rem .5rem .4rem;
  background: linear-gradient(180deg, transparent, rgba(0,0,0,.72));
  color: #fff;
  font-size: .78rem;
  line-height: 1.25;
  text-align: center;
  /* Two lines max, then ellipsis - the tile is small and the tooltip still
     carries the full name via <a title>. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
body.msh .msh-qv-grid img {
  display: block;
  width: 100%;
  /* Square, matching the /sq/ crop the collection tile itself shows, so the
     preview is the same composition the visitor hovered. */
  aspect-ratio: 1 / 1;
  object-fit: cover;
  transition: transform .3s var(--ease-out);
}
body.msh .msh-qv-cell:hover img { transform: scale(1.05); }
body.msh .msh-qv-cell:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }

/* Placeholder tiles, shown while the fetch is in flight so the box opens at
   its final height instead of shoving the CTA down when pictures arrive. */
body.msh .msh-qv-skel {
  display: block;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius);
  background: var(--paper-3);
  animation: msh-qv-pulse 1.2s var(--ease) infinite;
}
@keyframes msh-qv-pulse { 0%,100% { opacity: .5; } 50% { opacity: .9; } }

/* Nothing to preview (network error, or a collection whose contents this
   visitor may not see) - one image, the cover, spanning the box. */
/* Solo never scrolls (one image, already height-capped below), so it gets
   none of the scroll-shadow background the multi-picture grid above uses -
   left on, those faint edge gradients would just sit inert against a single
   cover image that never moves. */
body.msh .msh-qv-grid--solo { display: block; background: none; }
body.msh .msh-qv-grid--solo img { aspect-ratio: auto; max-height: 55vh; max-height: 55dvh; width: auto; margin-inline: auto; }

@media (max-width: 560px) {
  body.msh .msh-qv-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-qv-skel { animation: none; }
  body.msh .msh-qv-grid img,
  body.msh .msh-qv-cell:hover img { transition: none; transform: none; }
}
body.msh .msh-qv-x {
  position: absolute;
  top: -14px;
  right: -14px;
  width: 40px;
  height: 40px;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--card);
  color: var(--ink);
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: var(--sh-1);
}
body.msh .msh-qv-x:hover { background: var(--ink); color: var(--paper); }
@media (max-width: 600px) {
  body.msh .msh-qv-x { top: 4px; right: 4px; }
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-qv { animation: none; }
  body.msh .msh-quick { transition: none; }
}

/* ==========================================================================
   NEW ANIMATIONS (added 2026-08-20)

   1. Hero Mosaic Tile Entrance — staggered zoom + fade
   2. Carousel Smooth Transitions — Glider.js card slide
   3. Success Checkmark — checkout confirmation animation
   4. Loading Spinner — CSS-only for async operations
   ========================================================================== */

/* --- Hero Mosaic Tile Entrance ----------------------------------------------
   Four tiles (.msh-mo-1 through .msh-mo-4) enter with a staggered zoom + fade
   effect. Each tile starts offset and scaled, then rises and zooms to full size.
   The first tile (LCP) has animation: both so it starts invisible until the
   keyframes run (see msh-rise comment above: both prevents content from being
   permanently hidden with reduced motion). */
@keyframes msh-mo-zoom {
  from {
    opacity: 0;
    transform: scale(0.85) translateY(12px);
  }
  to {
    opacity: 1;
    transform: scale(1) translateY(0);
  }
}
body.msh .msh-mo-1 { animation: msh-mo-zoom .65s var(--ease-out) .2s both; }
body.msh .msh-mo-2 { animation: msh-mo-zoom .65s var(--ease-out) .35s both; }
body.msh .msh-mo-3 { animation: msh-mo-zoom .65s var(--ease-out) .5s both; }
body.msh .msh-mo-4 { animation: msh-mo-zoom .65s var(--ease-out) .65s both; }
/* Solo mosaic (when fewer than 4 trending images) uses the existing fade. */
body.msh .msh-mo-solo { animation: msh-fade .6s var(--ease) both; }

/* --- Carousel/Glider Smooth Transitions ------------------------------------
   Glider.js slides with a smooth glide effect. Added transition to the track
   so cards enter/exit gracefully. The .glider-track is what Glider moves via
   transform, so we animate it. Cards themselves get a subtle fade + lift on
   entrance (scroll-reveal handles this via [data-reveal]). */
body.msh .glider-track {
  transition: transform .5s var(--ease-out);
}
/* Dragging disables transitions so immediate feedback feels responsive. */
body.msh .glider.dragging .glider-track {
  transition: none;
}
/* Individual card entrance when Glider first loads - fade + scale.
   :not([data-reveal]) matches enhanceCarousel()'s guard in msh.js: a card
   the scroll-reveal observer owns must NOT also run this animation - the
   animation's opacity would override [data-reveal]'s opacity:0 while it
   plays and the card would flash in, finish, then vanish until observed. */
body.msh .glider .msh-card:not([data-reveal]) {
  animation: msh-card-in .45s var(--ease-out) backwards;
  animation-delay: calc(var(--card-index, 0) * 50ms);
}
@keyframes msh-card-in {
  from {
    opacity: 0;
    transform: scale(0.92);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}
/* Subtle image zoom on hover (existing) enhanced with longer duration. */
body.msh .glider .msh-card-media img:hover {
  transform: scale(1.08);
}

/* --- Success Checkmark Animation -------------------------------------------
   Multi-part confirmation: checkmark draws in, circle unfolds, success banner
   slides up. Used on checkout success pages and order confirmation screens.

   Apply .msh-success-state to a container with markup:
   <div class="msh-success-state">
     <div class="msh-check-circle">
       <svg class="msh-check-icon" viewBox="0 0 52 52">
         <circle class="msh-check-bg" cx="26" cy="26" r="25"/>
         <polyline class="msh-check-mark" points="13,26 24,35 39,17"/>
       </svg>
     </div>
     <div class="msh-success-message">
       <h2>Order Confirmed!</h2>
       <p>Your order has been placed successfully.</p>
     </div>
   </div>
*/
@keyframes msh-check-scale {
  0% { transform: scale(0); opacity: 0; }
  50% { transform: scale(1.15); }
  100% { transform: scale(1); opacity: 1; }
}
@keyframes msh-check-mark-draw {
  0% {
    stroke-dasharray: 48;
    stroke-dashoffset: 48;
  }
  100% {
    stroke-dasharray: 48;
    stroke-dashoffset: 0;
  }
}
@keyframes msh-success-msg-in {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
body.msh .msh-check-circle {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 auto 2rem;
}
body.msh .msh-check-icon {
  width: 80px;
  height: 80px;
  animation: msh-check-scale .6s var(--ease-out);
}
body.msh .msh-check-bg {
  fill: none;
  stroke: var(--brass);
  stroke-width: 2;
}
body.msh .msh-check-mark {
  fill: none;
  stroke: var(--brass);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  animation: msh-check-mark-draw .5s var(--ease-out) .3s both;
}
body.msh .msh-success-message {
  text-align: center;
  animation: msh-success-msg-in .5s var(--ease-out) .2s both;
}
body.msh .msh-success-message h2 {
  font-size: 1.75rem;
  color: var(--ink);
  margin: 0 0 .5rem;
}
body.msh .msh-success-message p {
  font-size: 1rem;
  color: var(--ink-2);
  margin: 0;
}
body.msh .msh-success-state {
  padding: 3rem 2rem;
  background: linear-gradient(135deg, var(--paper) 0%, var(--paper-2) 100%);
  border-radius: var(--radius);
  box-shadow: var(--sh-2);
}

/* --- Modal pop keyframes ----------------------------------------------------
   Used by msh.js showSuccess() (window.mshSuccess), which sets the animation
   inline on the success dialog it builds - forwards on open, reverse on
   close. (The rest of the old Loading Spinner family - .msh-spinner,
   .msh-loading-text, .msh-spinner-overlay/-box - was removed in the 20 Aug
   audit along with its never-called mshSpinner/mshInlineSpinner helpers;
   reintroduce spinner CSS together with a real consumer.) */
@keyframes msh-modal-pop {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

/* --- Image Zoom on Detail View -----------------------------------------------
   Product images zoom smoothly from thumbnail to detail view.
   Applied to product images on pages/media_detail.php. */
@keyframes msh-img-zoom {
  from {
    opacity: 0;
    transform: scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}
body.msh .msh-detail-img-main {
  animation: msh-img-zoom 0.5s var(--ease-out);
}

/* --- Grid Stagger Improve -------------------------------------------------------
   Enhanced scroll-reveal for gallery grids: when the observer marks a cell
   .is-in it rises via the msh-rise keyframes instead of the default
   transition. Only the .is-in state animates - a rule on the bare
   [data-reveal] cell would run immediately on load ('both' fills opacity:1),
   self-revealing the grid before the observer fires and defeating the
   stagger. */
body.msh [id^="dmcsgrid"] > div[data-reveal].is-in {
  animation: msh-rise 0.6s var(--ease-out) both;
  animation-delay: var(--reveal-delay, 0ms);
}

/* --- Breadcrumb Reveal ----------------------------------------------------------
   Breadcrumb trail items reveal left-to-right with fade + slide.
   Apply [data-breadcrumb] to container, [data-breadcrumb-item] to each item. */
@keyframes msh-breadcrumb-in {
  from {
    opacity: 0;
    transform: translateX(-8px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}
body.msh [data-breadcrumb-item] {
  animation: msh-breadcrumb-in 0.4s var(--ease-out) backwards;
}
body.msh [data-breadcrumb-item]:nth-child(1) { animation-delay: 0ms; }
body.msh [data-breadcrumb-item]:nth-child(2) { animation-delay: 80ms; }
body.msh [data-breadcrumb-item]:nth-child(3) { animation-delay: 160ms; }
body.msh [data-breadcrumb-item]:nth-child(4) { animation-delay: 240ms; }
body.msh [data-breadcrumb-item]:nth-child(n+5) { animation-delay: 320ms; }

/* --- Form Field Focus Glow -------------------------------------------------------
   Text inputs, selects, textareas glow on focus with smooth transition.
   Opt-in: wrap form fields in .msh-form-field, or apply .msh-focus-glow to inputs. */
body.msh .msh-form-field input,
body.msh .msh-form-field select,
body.msh .msh-form-field textarea,
body.msh input.msh-focus-glow,
body.msh select.msh-focus-glow,
body.msh textarea.msh-focus-glow {
  transition: border-color 0.25s var(--ease-out), box-shadow 0.25s var(--ease-out);
}
body.msh .msh-form-field input:focus,
body.msh .msh-form-field select:focus,
body.msh .msh-form-field textarea:focus,
body.msh input.msh-focus-glow:focus,
body.msh select.msh-focus-glow:focus,
body.msh textarea.msh-focus-glow:focus {
  border-color: var(--brass) !important;
  outline: none;
  box-shadow: 0 0 0 3px rgba(176, 132, 47, 0.1), inset 0 0 0 1px var(--brass);
}

/* --- Toast Notification Slide-In -----------------------------------------------
   Toasts appear from top/bottom with slide + fade. Fixed position, auto-dismiss.
   Classes: .msh-toast (default/info), .msh-toast--success, .msh-toast--error,
   .msh-toast--warning. JS creates/destroys them. */
@keyframes msh-toast-in {
  from {
    opacity: 0;
    transform: translateY(-12px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
@keyframes msh-toast-out {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-12px);
  }
}
body.msh .msh-toast {
  position: fixed;
  top: 20px;
  right: 20px;
  max-width: 420px;
  padding: 1rem 1.5rem;
  background: var(--card);
  border-left: 4px solid var(--ink-3);
  border-radius: 6px;
  box-shadow: var(--sh-2);
  animation: msh-toast-in 0.4s var(--ease-out);
  z-index: 9998;
  font-size: 0.95rem;
  line-height: 1.4;
}
body.msh .msh-toast.msh-toast--success {
  border-left-color: var(--sage);
  color: var(--sage);
}
body.msh .msh-toast.msh-toast--error {
  border-left-color: var(--terracotta);
  color: var(--terracotta);
}
body.msh .msh-toast.msh-toast--warning {
  border-left-color: #D4A574;
  color: #8B5A2B;
}
body.msh .msh-toast.is-closing {
  animation: msh-toast-out 0.3s var(--ease-out);
}
body.msh .msh-toast-close {
  position: absolute;
  top: 0.5rem;
  right: 0.75rem;
  background: none;
  border: none;
  color: inherit;
  cursor: pointer;
  font-size: 1.2rem;
  opacity: 0.6;
  transition: opacity 0.2s;
  padding: 0;
  width: 20px;
  height: 20px;
}
body.msh .msh-toast-close:hover {
  opacity: 1;
}

/* --- Error Shake Animation -------------------------------------------------------
   Form validation errors shake the field for attention.
   Apply .msh-error-shake to an input/container when validation fails. */
@keyframes msh-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}
body.msh .msh-error-shake {
  animation: msh-shake 0.4s var(--ease-out);
  border-color: var(--terracotta) !important;
  background-color: rgba(178, 80, 47, 0.03) !important;
}
body.msh .msh-error-msg {
  color: var(--terracotta);
  font-size: 0.85rem;
  margin-top: 0.25rem;
  animation: msh-rise 0.3s var(--ease-out);
}

/* Reduced motion: disable the animations of everything this section still
   ships (the selector list tracks the surviving components - the audit
   removed the spinner/progress/pagination/page-fade/parallax families). */
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-mo-1,
  body.msh .msh-mo-2,
  body.msh .msh-mo-3,
  body.msh .msh-mo-4,
  body.msh .msh-mo-solo,
  body.msh .glider .msh-card,
  body.msh .msh-check-icon,
  body.msh .msh-check-mark,
  body.msh .msh-success-message,
  body.msh .msh-success-state,
  body.msh .msh-detail-img-main,
  body.msh [id^="dmcsgrid"] > div[data-reveal],
  body.msh [data-breadcrumb-item],
  body.msh .msh-form-field input:focus,
  body.msh .msh-form-field select:focus,
  body.msh .msh-form-field textarea:focus,
  body.msh input.msh-focus-glow:focus,
  body.msh select.msh-focus-glow:focus,
  body.msh textarea.msh-focus-glow:focus,
  body.msh .msh-toast,
  body.msh .msh-error-shake,
  body.msh .msh-error-msg {
    animation: none;
    transition: none;
  }
  body.msh .glider-track { transition: none; }
  body.msh .msh-check-mark { stroke-dashoffset: 0; }
  body.msh .msh-form-field input:focus,
  body.msh .msh-form-field select:focus,
  body.msh .msh-form-field textarea:focus,
  body.msh input.msh-focus-glow:focus,
  body.msh select.msh-focus-glow:focus,
  body.msh textarea.msh-focus-glow:focus {
    box-shadow: none;
  }
}

/* --- Gallery pagination --------------------------------------------------
   thumb_grid.php emits <div class="pagination"><ul class="pagination"> - the
   class is on both the wrapper AND the list, so anything targeting
   .pagination hits twice. Scoped to ul.pagination / div.pagination so the
   two get different treatment instead of fighting. */
body.msh div.pagination { margin: clamp(2rem, 4vw, 3rem) 0 !important; }
body.msh ul.pagination {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: .4rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
body.msh ul.pagination li { margin: 0; }
body.msh ul.pagination a {
  display: grid;
  place-items: center;
  min-width: 42px;
  min-height: 42px;          /* comfortable touch target */
  padding: 0 .6rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink-2);
  font-size: .875rem;
  font-weight: 500;
  text-decoration: none;
  transition: border-color .2s var(--ease), background .2s var(--ease);
}
body.msh ul.pagination a:hover {
  border-color: var(--ink);
  background: var(--paper-2);
  color: var(--ink);
}
/* The current page is an <a> with no href, so it must not look clickable. */
body.msh ul.pagination a.currentpage,
body.msh ul.pagination a.currentpage:hover {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
  cursor: default;
  font-weight: 600;
}

/* --- Search filter pills (thumb_grid.php's refine bar) -------------------
   The pills and their option panels are emitted by thumb_grid.php for every
   template with inline styles, so everything here is msh-only progressive
   polish over those. Two behaviours are corrected, not just styled:
   1. The option panel used to render in the page flow (position:static),
      shoving the entire results grid down every time a pill was opened -
      anchoring it to the .dmcs-filter wrapper makes it a real dropdown.
   2. The option rows are focusable divs (tabindex=0/role=button, activated
      via the delegated keydown handler thumb_grid.php emits), so they need
      visible hover/focus states here.
   Open/close/aria-expanded logic lives in thumb_grid.php's dmcsFilterToggle(). */
body.msh .dmcs-filter { position: relative; }
body.msh .dmcs-filter-toggle {
  border-color: var(--line-2) !important;
  color: var(--ink-2) !important;
  transition: border-color .2s var(--ease), color .2s var(--ease);
}
body.msh .dmcs-filter-toggle:hover,
body.msh .dmcs-filter-toggle[aria-expanded="true"] {
  border-color: var(--ink) !important;
  color: var(--ink) !important;
}
body.msh .dmcs-filter-toggle strong { margin-left: .5rem; }
body.msh .dmcs-filter-panel {
  /* !important because two of the panels (Image Type / Content Type) carry
     inline position:absolute already and the rest are inline-static. */
  position: absolute !important;
  top: 100%;
  left: 10px;
  z-index: 60;
  min-width: 230px;
  max-width: calc(100vw - 2rem);
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: var(--sh-2);
  text-align: left;
}
body.msh .dmcs-filter-panel [role="button"] {
  display: flex;
  align-items: center;
  gap: .5rem;
  padding: .35rem .5rem;
  border-radius: 8px;
}
body.msh .dmcs-filter-panel [role="button"]:hover { background: var(--paper-2); }
body.msh .dmcs-filter-panel [role="button"]:focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 1px;
}

/* Result count line under the Search Results heading (thumb_grid.php). */
body.msh .dmcs-result-count {
  color: var(--ink-3) !important;
  font-size: .9375rem;
}

@media (max-width: 640px) {
  /* Two-up result grid: one 300px cell per row made comparing results a
     screen-per-item exercise. The cells and their frame images carry inline
     sizes from thumb_grid.php, hence the !important overrides. The padding is
     the frame "mat", slimmed down to suit the half-width frame. */
  body.msh [id^="dmcsgrid"] > div { width: calc(50% - 12px) !important; margin: 6px !important; }
  body.msh [id^="dmcsgrid"] > div img[id^="image_"] {
    max-width: 100% !important;
    max-height: none !important;
    padding: 16px !important;
  }
  /* Square /sq/ collection tiles (img.dmcsThumbGridCell - see thumb_grid.php
     ~line 2375) carry their own inline max-width:300px and were missed by the
     override above, so they kept rendering at full 300px width inside the
     183px half-width cell and spilled over the next column's thumbnail. No
     "mat" padding here - these tiles aren't framed prints. */
  body.msh [id^="dmcsgrid"] > div img.dmcsThumbGridCell {
    max-width: 100% !important;
  }
  /* Filter pills: three fixed-200px pills stacked vertically ate the whole
     first viewport before any result was visible - let them shrink to their
     labels and sit inline instead. */
  body.msh .dmcs-filter { padding: 4px !important; }
  body.msh .dmcs-filter-toggle { width: auto !important; }
}

/* --- Lightbox toggle on a grid cell -------------------------------------- */
body.msh .image-card {
  display: inline-flex !important;
  align-items: center;
  gap: .35rem;
  padding: .3rem .5rem;
  border-radius: var(--radius);
  font-size: .8125rem;
  color: var(--ink-2);
  transition: color .2s var(--ease);
}
body.msh .image-card:hover { color: var(--ink); }

/* --- Sort / filter panel ------------------------------------------------- */
body.msh #sortfilter {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  padding: clamp(.75rem, 2vw, 1.1rem);
  box-shadow: var(--sh-1);
}

/* --- Brand / partner logo strip ----------------------------------------- */
body.msh .msh-brands {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}
body.msh .msh-brands a {
  display: grid;
  place-items: center;
  min-height: 92px;
  padding: 1rem;
  background: var(--card);
  color: var(--ink-2);
  font-size: .8125rem;
  font-weight: 500;
  text-align: center;
  text-decoration: none;
  transition: background .2s var(--ease), color .2s var(--ease);
}
body.msh .msh-brands a:hover { background: var(--paper-2); color: var(--ink); }

/* --- Reassurance strip -------------------------------------------------- */
body.msh .msh-trust {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: clamp(1rem, 2.5vw, 2rem);
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
  border-block: 1px solid var(--line);
}
body.msh .msh-trust > div { display: flex; gap: .75rem; align-items: flex-start; }
body.msh .msh-trust b { display: block; font-size: .9rem; }
body.msh .msh-trust span { font-size: .8125rem; color: var(--ink-3); }

/* --- Footer ------------------------------------------------------------- */
body.msh footer {
  background: var(--foot-bg);
  border-top: 1px solid var(--line);
  color: var(--foot-text);
  margin-top: clamp(3rem, 7vw, 6rem);
  padding-block: clamp(2.5rem, 5vw, 4rem);
}
/* The real culprit for the footer colour. css/stylesheet.php sets
   .dmcsDivFullWidthFooter{background:$dmcsFooterColour;color:$dmcsFooterColourtext}
   - #f7f7f7 on #222 for this domain - and that div sits INSIDE <footer>, so
   it painted a cold grey slab straight over the warm paper tone set on the
   footer element. Styling the element alone was never going to win: the
   child paints last regardless of specificity. */
body.msh footer .dmcsDivFullWidthFooter {
  background: transparent;
  color: inherit;
  width: 100%;
  margin: 0 auto;
}
body.msh footer a { color: var(--foot-text); text-decoration: none; }
body.msh footer a:hover { color: var(--foot-text); text-decoration: underline; }
/* Legacy "FOOTER A{color:...}" and its :visited twin are element selectors
   carrying the old link colour; :visited in particular is easy to miss. */
body.msh footer a:visited { color: var(--foot-text); }
/* A <button> that must read as a footer link - currently the cookie-consent
   opener (pagefooter.php), which is a button because it opens a dialog
   rather than navigating. Mirrors the footer link rules above. */
body.msh footer button.msh-linklike {
  background: none;
  border: 0;
  padding: 0;
  margin: 0;
  font: inherit;
  color: var(--foot-text);
  cursor: pointer;
  text-align: left;
}
body.msh footer button.msh-linklike:hover { text-decoration: underline; }
body.msh footer button.msh-linklike:focus-visible {
  outline: 2px solid var(--brass-text);
  outline-offset: 2px;
}
body.msh .msh-foot-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: clamp(1.5rem, 4vw, 3rem);
}
/* Unbreakable strings in the narrow footer columns - the contact email
   address in particular - otherwise poke past their 190px column and widen
   the whole document, giving every mobile page a ~19px sideways wobble.
   "anywhere" lets them break mid-string only when nothing else fits. */
body.msh .msh-foot-grid a,
body.msh .msh-foot-grid p,
body.msh .msh-foot-grid li { overflow-wrap: anywhere; }
/* Font now comes entirely from the sitewide h3 rule (was its own small
   uppercase label style before h3 became a single universal look). Margin/
   border still neutralised: each of these is the first thing in its own
   footer grid column, so h3's sitewide top margin/border-top (meant for a
   divider between blocks of content) would misalign the columns against
   each other and draw a stray line above only some of them. */
body.msh .msh-foot-grid h3 { margin: 0 0 .85rem; padding-top: 0; border-top: none; }
body.msh .msh-foot-grid ul { list-style: none; margin: 0; padding: 0; }
body.msh .msh-foot-grid li { margin-bottom: .45rem; font-size: calc(.875rem * var(--msh-fs)); }
/* The footer's first column carries the logo, blurb and subscribe form, so
   it gets more room than the link columns. */
body.msh .msh-foot-intro { grid-column: span 2; max-width: 420px; }
body.msh .msh-foot-intro p { margin: 0 0 .85rem; font-size: calc(.875rem * var(--msh-fs)); }
body.msh .msh-foot-intro img { height: 46px; width: auto; }

body.msh .msh-subscribe {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
  margin: .25rem 0 1rem;
}
body.msh .msh-subscribe input[type="email"] {
  flex: 1 1 190px;
  min-width: 0;
  min-height: 46px;
  padding: .6rem .9rem;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--card);
  font: inherit;
  font-size: calc(.9rem * var(--msh-fs));
  color: var(--ink);
}
body.msh .msh-subscribe input[type="email"]:focus {
  outline: 0;
  border-color: var(--brass);
  box-shadow: 0 0 0 3px rgba(176,132,47,.16);
}

/* Visible to screen readers, not on screen - used for labels whose visual
   role is already carried by a placeholder. */
body.msh .msh-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 760px) {
  body.msh .msh-foot-intro { grid-column: span 1; }
}

/* --- Header/footer: shared components ----------------------------------- */
/* Two components are styled once for the whole site but also appear inside the
   two bands: the button and the h3. Their sitewide rules are left alone (they
   size main-page content, which follows body copy, not --msh-fs) and re-stated
   here scoped to the bands, at the same designed rem values, so the toolbar's
   Log Out/Basket buttons, the footer's Subscribe button, the footer column
   headings and the basket drawer's title move with the rest of their band.
   Scoped to header/footer, so more specific than the sitewide rules and later
   in the file - it wins on both counts. */
body.msh header .msh-btn,
body.msh footer .msh-btn { font-size: calc(.9375rem * var(--msh-fs)); }
body.msh header h3,
body.msh footer h3 { font-size: calc(1.3rem * var(--msh-fs)); }

/* --- Scroll reveal ------------------------------------------------------ */
body.msh [data-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .7s var(--ease-out), transform .7s var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}
body.msh [data-reveal].is-in { opacity: 1; transform: none; }

/* Anyone who has asked for less motion gets none of it, and nothing is
   left invisible waiting on an animation that will never run. */
@media (prefers-reduced-motion: reduce) {
  body.msh *,
  body.msh *::before,
  body.msh *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    /* Delays too: the hero entrance animations use `both` fill, so during a
       delay the element sits at its from-state (invisible). Zeroing only the
       duration would still hold content blank for the delay period. */
    animation-delay: 0ms !important;
    transition-duration: .001ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }
  body.msh [data-reveal] { opacity: 1; transform: none; }
  body.msh .msh-ticker-track { animation: none; }
}

/* --- Responsive --------------------------------------------------------- */
@media (max-width: 1024px) {
  body.msh .msh-hero { grid-template-columns: 1fr; }
}

@media (max-width: 760px) {
  body.msh .msh-bar { flex-wrap: wrap; padding-block: .65rem; row-gap: .6rem; }
  body.msh .msh-brand { order: 1; }
  body.msh .msh-actions { order: 2; margin-left: auto; }
  body.msh .msh-search { order: 3; flex-basis: 100%; }
  body.msh .msh-brand img { height: 36px; }
  body.msh .glider-prev { left: 0; }
  body.msh .glider-next { right: 0; }
}

/* --- Phones -------------------------------------------------------------
   Phones receive this same chrome as desktop rather than a separate m-site
   header, so the sticky region has to earn its height: ticker + bar + nav
   stacked would eat a third of a small viewport before any content. */
@media (max-width: 600px) {
  /* The ticker is decorative reassurance; the space is worth more on a
     phone. Its content is repeated in the footer strip. */
  body.msh .msh-ticker { display: none; }

  body.msh .msh-bar { padding-block: .5rem; gap: .5rem; }
  body.msh .msh-brand img { height: 32px; }

  /* Search sits on its own row and is the widest target on the page. */
  body.msh form#searchform { padding-left: .9rem; }
  /* max(), not a flat 16px: 16px is the floor iOS needs to leave the page
     alone on focus, but a domain that has scaled its chrome up should still
     get the bigger field - so take whichever is larger. */
  body.msh form#searchform input[type="search"] { font-size: max(16px, calc(.95rem * var(--msh-fs))); }

  body.msh .msh-nav-inner { gap: 1.1rem; }
  body.msh .msh-nav-inner > a { font-size: calc(.875rem * var(--msh-fs)); }

  /* Two-up thumbnails with a real gutter rather than three cramped ones. */
  body.msh div.dmcsThumbGrid,
  body.msh div.dmcsThumbGridGallery,
  body.msh div.dmcsThumbGridSearch { width: 46%; margin: 0 1.5% 1.25rem; }

  body.msh .glider .msh-card { width: 220px; }

  body.msh .msh-pdp,
  body.msh .msh-pdp-buy { padding-inline: 0; }
  body.msh .msh-pdp-buy { border-left: 0; border-right: 0; border-radius: 0; }

  body.msh .msh-range-item { padding: 1.1rem; }
  body.msh .msh-trust { gap: 1.25rem; }
}

/* Very small screens: single column beats two cramped ones. */
@media (max-width: 380px) {
  body.msh div.dmcsThumbGrid,
  body.msh div.dmcsThumbGridGallery,
  body.msh div.dmcsThumbGridSearch { width: 100%; margin: 0 0 1.25rem; }
}

/* Coarse pointers get permanently visible affordances rather than
   hover-revealed ones, and comfortable hit areas. */
@media (hover: none), (pointer: coarse) {
  body.msh .msh-nav-inner > a { padding-block: 1rem; }
  body.msh .msh-actions > a { width: 46px; height: 46px; }
}

/* --- Legacy layout wrappers ---------------------------------------------
   Seen only in the browser: the page rendered as a white content column on a
   grey field, because css/stylesheet.php paints .dmcsMiddle / .dmcsDivMid /
   .dmcsDivToolbar with the legacy foreground colour and the body shows
   through around them. The redesign is one continuous paper surface, so
   these are made transparent and their width caps removed. */
body.msh .dmcsMiddle,
body.msh .dmcsDivMid,
body.msh .dmcsDivToolbar,
body.msh .dmcsDivFullWidthFooter {
  background: transparent !important;
  box-shadow: none !important;
  border: 0 !important;
  /* NB: width caps deliberately NOT removed here. Stripping max-width from
     these wrappers did kill the grey gutter, but it also let the Glider
     filmstrips bleed to the viewport edges while every heading still stopped
     at the 1440px gutter - the strips and their own headings no longer lined
     up. Only the PAINT is removed; the measure stays. */
}

/* One measure for the whole page. Every section - headings and filmstrips
   alike - resolves to the same max-width and the same gutter, so there is a
   single left edge down the page instead of three. */
body.msh .msh-section-head,
body.msh .msh-section-head--solo,
body.msh .glider-contain,
body.msh .msh-fmtpanels,
body.msh .msh-brands {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--gutter);
  box-sizing: border-box;
}
/* ...except where the element is already inside a .msh-wrap, which has
   applied the gutter once already. */
body.msh .msh-wrap .msh-section-head,
body.msh .msh-wrap .glider-contain,
body.msh .msh-wrap .msh-fmtpanels,
body.msh .msh-wrap .msh-brands { padding-inline: 0; max-width: none; }

/* --- Gallery filter/sort toolbar (built by galleryToolbar() in msh.js) ----
   Prototype: D:\www\new collection.html .toolbar. Sticks under the site
   header; msh.js sets --msh-stick-top to the header's live height because
   the header wrapper is position:sticky with a height that varies with
   viewport width. */
body.msh .msh-gtoolbar {
  position: sticky;
  top: var(--msh-stick-top, 0px);
  z-index: 40;                      /* under the header wrapper's 100 */
  background: color-mix(in srgb, var(--paper) 92%, transparent);
  backdrop-filter: blur(10px);
  border-block: 1px solid var(--line);
  margin-block: 1rem;
}
body.msh .msh-gtoolbar-in {
  max-width: var(--maxw);
  margin-inline: auto;
  padding: .65rem var(--gutter);
  display: flex;
  gap: .75rem 1rem;
  align-items: center;
  flex-wrap: wrap;
}
body.msh .msh-gtoolbar .msh-chips {
  display: flex;
  gap: .5rem;
  overflow-x: auto;
  scrollbar-width: none;
  flex: 1 1 auto;
  min-width: 0;
}
body.msh .msh-gtoolbar .msh-chips::-webkit-scrollbar { display: none; }
body.msh .msh-gtoolbar .msh-chip {
  display: inline-flex;
  align-items: center;
  padding: .44rem .9rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: transparent;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 500;
  color: var(--ink-2);
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .3s var(--ease), color .3s var(--ease),
              background .3s var(--ease), transform .3s var(--ease);
}
body.msh .msh-gtoolbar .msh-chip:hover {
  border-color: var(--ink);
  color: var(--ink);
  transform: translateY(-1px);
}
body.msh .msh-gtoolbar .msh-chip[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
body.msh .msh-gtoolbar .msh-gt-ctl {
  display: flex;
  gap: .5rem;
  align-items: center;
  flex-wrap: wrap;
}
body.msh .msh-gtoolbar select {
  appearance: none;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  padding: .44rem 2rem .44rem .9rem;
  background: transparent;
  font-family: var(--sans);
  font-size: .78rem;
  color: var(--ink-2);
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none' stroke='%23786C5E' stroke-width='1.5'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .8rem center;
}
body.msh .msh-gtoolbar select:focus-visible,
body.msh .msh-gtoolbar .msh-chip:focus-visible,
body.msh .msh-gtoolbar .msh-gt-clear:focus-visible {
  outline: 2px solid var(--brass-text);
  outline-offset: 2px;
}
body.msh .msh-gtoolbar .msh-gt-clear {
  border: 0;
  background: none;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 600;
  color: var(--brass-text);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 3px;
  padding: .44rem .25rem;
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-gtoolbar .msh-chip { transition: none; }
}

/* --- Sub-gallery collection rail (thumb_grid.php) ------------------------
   Replaces the legacy alphabetical chip wall on gallery pages that carry
   child collections (e.g. /europe/italy): a horizontally scrollable strip
   of .msh-card collection cards (square cover + title + item count), most
   populated first, followed by an "All N collections" toggle that reveals
   the complete alphabetical list as compact pill links. The pills reuse the
   .msh-gtoolbar .msh-chip look above; the cards reuse .msh-card-media/-title/
   -meta, so this section only carries the rail geometry and the two pieces
   that are new: the dashed "all" tile and the expandable list. */
body.msh .msh-subgal {
  max-width: var(--maxw);
  margin: 1.25rem auto 0;
  padding-inline: var(--gutter);
  box-sizing: border-box;
  text-align: left;
}
body.msh h2.msh-subgal-label {
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 .7rem;
}
body.msh .msh-subgal-rail {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x proximity;
  padding-bottom: .6rem;               /* keeps the scrollbar off the captions */
  scrollbar-width: thin;
  scrollbar-color: var(--line-2) transparent;
}
body.msh .msh-subgal-card {
  flex: 0 0 auto;
  width: 150px;
  scroll-snap-align: start;
}
body.msh .msh-subgal-card .msh-card-title {
  margin-top: .5rem;
  font-size: .85rem;
}
body.msh .msh-subgal-more {
  flex: 0 0 auto;
  width: 150px;
  aspect-ratio: 1 / 1;
  scroll-snap-align: start;
  border: 1px dashed var(--line-2);
  border-radius: var(--radius);
  background: transparent;
  font-family: var(--sans);
  font-size: .85rem;
  font-weight: 600;
  color: var(--brass-text);
  cursor: pointer;
  transition: border-color .3s var(--ease), color .3s var(--ease);
}
body.msh .msh-subgal-more:hover {
  border-color: var(--ink);
  color: var(--ink);
}
body.msh .msh-subgal-more:focus-visible {
  outline: 2px solid var(--brass-text);
  outline-offset: 2px;
}
body.msh .msh-subgal-all {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  margin-top: .5rem;
}
/* display:flex above would defeat the hidden attribute the toggle relies on */
body.msh .msh-subgal-all[hidden] { display: none; }
body.msh .msh-subgal-all a {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .44rem .9rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 500;
  color: var(--ink-2);
  text-decoration: none;
  white-space: nowrap;
}
body.msh .msh-subgal-all a:hover {
  border-color: var(--ink);
  color: var(--ink);
}
body.msh .msh-subgal-all a:focus-visible {
  outline: 2px solid var(--brass-text);
  outline-offset: 2px;
}
body.msh .msh-subgal-all a span {
  color: var(--ink-3);
  font-weight: 400;
}
@media (max-width: 700px) {
  body.msh .msh-subgal-card,
  body.msh .msh-subgal-more { width: 122px; }
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-subgal-more { transition: none; }
}

/* --- Print-range family filter chips (built by productToolbar() in
   msh.js) ------------------------------------------------------------
   Prototype: D:\www\dmcs\derek1 products.html .toolbar#famFilters. Unlike
   the gallery toolbar this one is plain document flow (not sticky) and
   purely client-side - it show/hides the already-rendered .msh-range-item
   cards, it does not round-trip to the server. Chip look reuses the same
   pill styling as .msh-gtoolbar .msh-chip above for visual consistency. */
body.msh .msh-rtoolbar {
  margin-block: 1.25rem 1.75rem;
}
body.msh .msh-rtoolbar .msh-chips {
  display: flex;
  gap: .5rem;
  flex-wrap: wrap;
}
body.msh .msh-rtoolbar .msh-chip {
  display: inline-flex;
  align-items: center;
  padding: .44rem .9rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: transparent;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 500;
  color: var(--ink-2);
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .3s var(--ease), color .3s var(--ease),
              background .3s var(--ease), transform .3s var(--ease);
}
body.msh .msh-rtoolbar .msh-chip:hover {
  border-color: var(--ink);
  color: var(--ink);
  transform: translateY(-1px);
}
body.msh .msh-rtoolbar .msh-chip[aria-pressed="true"] {
  background: var(--ink);
  color: var(--paper);
  border-color: var(--ink);
}
body.msh .msh-rtoolbar .msh-chip:focus-visible {
  outline: 2px solid var(--brass-text);
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-rtoolbar .msh-chip { transition: none; }
}

/* --- Cart slide-in drawer (built by cartDrawer() in msh.js) ------------
   Prototype: D:\www\dmcs2\derek1 assets/css/main.css .scrim/.drawer/
   .cart-item (renamed msh-scrim/msh-drawer/msh-cart-item so they can't
   collide with any legacy .cart-* class elsewhere in the old templates).
   Real basket contents are fetched from api/basket_preview.php - this
   block is layout/animation only. */
body.msh .msh-scrim {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(22,19,15,.42);
  backdrop-filter: blur(3px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .4s var(--ease);
}
body.msh .msh-scrim.is-open { opacity: 1; pointer-events: auto; }
body.msh .msh-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 210;
  width: min(440px, 92vw);
  background: var(--paper);
  display: flex;
  flex-direction: column;
  transform: translateX(102%);
  transition: transform .55s var(--ease);
  box-shadow: var(--sh-3);
}
body.msh .msh-drawer.is-open { transform: none; }
body.msh .msh-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1.5rem;
  border-bottom: 1px solid var(--line);
  flex: 0 0 auto;
}
/* Font-size dropped (redundant - already h3's sitewide 1.3rem). Margin/
   border still neutralised: this h3 sits beside the close button in a
   horizontal flex row, not a standalone block, so h3's sitewide top
   margin/border-top would shove the title down and draw a stray line
   through the middle of the header bar. */
body.msh .msh-drawer-head h3 { margin: 0; padding-top: 0; border-top: none; }
body.msh .msh-drawer-close {
  border: 0;
  background: none;
  font-size: calc(1.75rem * var(--msh-fs));
  line-height: 1;
  color: var(--ink-2);
  cursor: pointer;
  padding: .25rem .5rem;
}
body.msh .msh-drawer-close:hover { color: var(--ink); }
body.msh .msh-drawer-body { flex: 1 1 auto; overflow-y: auto; padding: 0 1.5rem; }
body.msh .msh-drawer-foot {
  flex: 0 0 auto;
  border-top: 1px solid var(--line);
  padding: 1.5rem;
  background: var(--paper-2);
}
body.msh .msh-drawer-subtotal {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  margin-bottom: 1rem;
  font-size: calc(.9375rem * var(--msh-fs));
}
body.msh .msh-drawer-subtotal b { font-size: calc(1.15rem * var(--msh-fs)); }
body.msh .msh-cart-item {
  display: grid;
  grid-template-columns: 64px 1fr auto;
  gap: 1rem;
  padding: 1.25rem 0;
  border-bottom: 1px solid var(--line);
  align-items: start;
}
body.msh .msh-cart-item img {
  width: 64px;
  height: 78px;
  object-fit: cover;
  border-radius: var(--radius);
  background: var(--paper-3);
}
body.msh .msh-cart-item-title { font-family: var(--sans); font-size: calc(.95rem * var(--msh-fs)); font-weight: 400; line-height: 1.3; } /* 13 Aug: --serif -> --sans, see .msh-price-now comment */
body.msh .msh-cart-item-meta { font-size: calc(.8125rem * var(--msh-fs)); color: var(--ink-3); margin-top: .25rem; }
body.msh .msh-cart-item-price { font-weight: 600; white-space: nowrap; }
body.msh .msh-cart-empty { text-align: center; padding: 3.5rem 1rem; color: var(--ink-3); }
body.msh .msh-cart-empty a { color: var(--link); font-weight: 600; }
@media (max-width: 600px) {
  body.msh .msh-drawer { width: 100vw; }
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-drawer { transition: none; }
  body.msh .msh-scrim { transition: none; }
}

/* --- Jigsaw piece-cut effect + playable demo (built by jigsawBoard()/
   jigsawDemo() in msh.js, markup from pages/jigsaw-puzzles.php) ---
   Prototype: D:\www\dmcs2\derek1 assets/css/main.css .jigboard, .jig-bevel,
   .jig-kerf, .jig-loose, .jig-hole, .demo-grid, .demo-cell classes
   (renamed to an msh-jig- and msh-demo- prefix here so nothing collides
   with a legacy "jig" or "demo" class elsewhere in the old templates,
   and state classes are is-sel/is-over/is-dragging/is-done rather than
   the prototype's bare sel/over/dragging/done for the same reason). The
   cut geometry itself (SVG path data) is generated at runtime - this
   block is layout/paint only. */
body.msh .msh-jigboard {
  position: relative;
  aspect-ratio: 4/3;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-3);
  box-shadow: 0 2px 4px rgba(22,19,15,.14), 0 18px 40px rgba(22,19,15,.18);
}
body.msh .msh-jigboard img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
body.msh .msh-jigboard svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

/* A cut has two parts: a pale bevelled shoulder either side, and a dark
   kerf between the pieces - drawing both is what stops it reading as a
   single drawn line. */
body.msh .msh-jig-bevel { fill: none; stroke: rgba(255,255,255,.34); stroke-width: 2.6; vector-effect: non-scaling-stroke; stroke-linejoin: round; }
body.msh .msh-jig-kerf  { fill: none; stroke: rgba(28,22,16,.55);   stroke-width: .9;  vector-effect: non-scaling-stroke; stroke-linejoin: round; }

/* Satin laminate sheen across the assembled board. */
body.msh .msh-jigboard::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(112deg,
    rgba(255,255,255,.20) 0%, rgba(255,255,255,.04) 22%,
    transparent 45%, rgba(255,255,255,.09) 72%, rgba(255,255,255,.02) 100%);
}

/* Loose pieces lifted clear of the board, and the holes they leave. */
body.msh .msh-jig-loose { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; z-index: 3; }
body.msh .msh-jig-loose .msh-jig-piece { filter: drop-shadow(0 3px 5px rgba(22,19,15,.42)); }
body.msh .msh-jig-piece-edge { fill: none; stroke: rgba(255,255,255,.5); stroke-width: 1.1; vector-effect: non-scaling-stroke; }
body.msh .msh-jig-hole { fill: #6E655A; }
body.msh .msh-jig-hole-shade { fill: rgba(0,0,0,.34); }

/* Piece-count chips: same pill look as .msh-rtoolbar .msh-chip, kept as its
   own small block since these live in the hero, not a filter toolbar. */
body.msh .msh-jig-pieces { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: 1.25rem; }
body.msh .msh-jig-pieces .msh-chip {
  display: inline-flex;
  align-items: center;
  padding: .44rem .9rem;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: transparent;
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 500;
  color: var(--ink-2);
  white-space: nowrap;
  cursor: pointer;
  transition: border-color .3s var(--ease), color .3s var(--ease), background .3s var(--ease);
}
body.msh .msh-jig-pieces .msh-chip:hover { border-color: var(--ink); color: var(--ink); }
body.msh .msh-jig-pieces .msh-chip[aria-pressed="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
body.msh .msh-jig-pieces .msh-chip:focus-visible { outline: 2px solid var(--brass-text); outline-offset: 2px; }

/* --- Playable nine-tile demo ---------------------------------------------
   Cells are built once and only their background-position changes, so a
   tile can be dragged without the DOM being torn out from under it. */
body.msh .msh-demo-grid {
  position: relative;
  aspect-ratio: 1;
  display: grid;
  grid-template-columns: repeat(3,1fr);
  grid-template-rows: repeat(3,1fr);
  gap: 3px;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--paper-3);
}
body.msh .msh-demo-cell {
  position: relative;
  overflow: hidden;
  cursor: grab;
  border-radius: var(--radius);
  background-size: 300% 300%;
  outline: 2px solid transparent;
  outline-offset: -2px;
  touch-action: none; /* let the pointer drag instead of scrolling the page */
  user-select: none; -webkit-user-select: none; -webkit-user-drag: none;
  transition: transform .35s var(--ease-out), box-shadow .3s, outline-color .3s;
}
body.msh .msh-demo-cell:hover { transform: scale(1.03); z-index: 2; box-shadow: var(--sh-2); }
body.msh .msh-demo-cell.is-sel { outline-color: var(--brass); z-index: 3; }
/* the tile in hand */
body.msh .msh-demo-cell.is-dragging {
  cursor: grabbing; z-index: 20; transition: none; pointer-events: none;
  box-shadow: var(--sh-3); outline-color: var(--brass);
  filter: brightness(1.04);
}
/* the tile it is hovering over */
body.msh .msh-demo-cell.is-over { outline-color: var(--ink); z-index: 3; transform: scale(.95); }
body.msh .msh-demo-cell.is-done { cursor: default; }
body.msh .msh-demo-grid:has(.is-dragging) .msh-demo-cell:hover { transform: none; box-shadow: none; }

@keyframes msh-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.06); }
  100% { transform: scale(1); }
}

@media (max-width: 900px) {
  body.msh .msh-jigboard { aspect-ratio: 3/2; }
}
@media (prefers-reduced-motion: reduce) {
  body.msh .msh-demo-cell { transition: none; }
  body.msh .msh-demo-cell.is-done { animation: none !important; }
}


/* --- Basket notice ------------------------------------------------------
   18 Aug 2026: pages/basket.php renders this when an add-to-basket was rejected and bounced
   the customer here (see basket_notice() there). Terracotta is the palette's only
   attention colour; used as a rule down the side rather than as a fill so the message reads
   as information rather than as an error page.
   ------------------------------------------------------------------------ */
body.msh .msh-basket-notice {
  max-width: 60ch;
  margin: 0 0 1.25rem;
  padding: .75rem 1rem;
  background: var(--paper-2);
  border-left: 3px solid var(--terracotta);
  border-radius: var(--radius);
  color: var(--ink-2);
  font-size: .9375rem;
  line-height: 1.5;
}

/* --- Inline links in body copy -------------------------------------------
   18 Aug 2026: pages/media_detail.php's shipping and returns sentences were already real
   anchors, but they inherited the body ink and carried no underline, so on the product page
   they were indistinguishable from the surrounding text. This is opt-in by class rather than
   a rule on ".dmcsDivMid a", so it cannot restyle body-copy links on other pages.
   --brass-text is the palette's 4.5:1 text accent, so this passes AA on paper and on card.
   ------------------------------------------------------------------------ */
body.msh a.msh-inline-link {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: .15em;
  text-decoration-thickness: from-font;
}
body.msh a.msh-inline-link:hover {
  color: var(--link-hover);
  text-decoration-thickness: 2px;
}

/* --- Bare links in body copy --------------------------------------------
   18 Aug 2026. Two rules used to combine to make links invisible: css/stylesheet.php:124
   (`A, A:visited{...text-decoration:none...}`, sitewide legacy, shared with every other site
   on the platform so not editable from here) removes the underline, and `body.msh a
   { color: inherit }` further up this file then removes the colour difference too. Net
   effect: an anchor with no class of its own was typographically identical to the sentence
   around it. Measured before this rule: 48 of 54 text links on the homepage, 21 of 22 on the
   basket, 19 of 20 at checkout.

   Scoped to class-less anchors inside `main`:
     - `:not([class])` so every existing treatment wins by default: .plain, .msh-more,
       .msh-btn, .breadcrumbs, .dmcsThumbGrid and friends are all untouched. In this design
       an anchor that is really a component (card, tile, panel, nav item) always carries a
       class, so "no class at all, inside the page body" is a good proxy for "a link in
       some copy".
     - inside `main`, so the toolbar and the footer keep their own band colours.
     - the :has() block below opts image links back out. If a browser does not support :has()
       that block is dropped and an anchor wrapping only an image picks up an underline it has
       no text to draw - harmless, and it fails in the safe direction.
     - explicit opt-outs for the two component contexts that DO hold class-less anchors, both
       at the bottom of this file: basket lines, and the pagination strip.
   18 Aug 2026, later the same day: the parent was originally pinned to `p >`, `li >` and
   `td >` on the argument that only running prose needed it, and that let real links through -
   measured on localhost, the FAQ page's "Returns Policy" link sits in a bare <div>, so it
   rendered as plain body ink with no underline, indistinguishable from the sentence around
   it, which is the exact bug this rule was written for. Widening to any class-less anchor in
   `main` changes nothing on the homepage or About Us (0 and 2 matches, both already in <p>)
   and picks that whole class of link up.
   Colour is the domain's own --link (see the token block at the top of this file); it used to
   be the fixed --brass-text gold, which ignored domain.colour_link entirely.
   ------------------------------------------------------------------------ */
body.msh main a:not([class]) {
  color: var(--link);
  text-decoration: underline;
  text-underline-offset: .15em;
  text-decoration-thickness: from-font;
}
body.msh main a:not([class]):hover {
  color: var(--link-hover);
  text-decoration-thickness: 2px;
}
body.msh main a:not([class]):has(img),
body.msh main a:not([class]):has(svg) {
  color: inherit;
  text-decoration: none;
}

/* --- Cart drawer discount row -------------------------------------------
   18 Aug 2026: combined-shipping refunds now appear in the drawer (msh.js addDiscount).
   Same grid as .msh-cart-item so the amount lines up with the prices above it; no thumbnail,
   no bottom rule, and the amount takes the accent so a credit does not read as a charge.
   ------------------------------------------------------------------------ */
body.msh .msh-cart-discount {
  border-bottom: 0;
  padding-top: .5rem;
}
body.msh .msh-cart-discount .msh-cart-item-title {
  color: var(--ink-3);
}
body.msh .msh-cart-discount .msh-cart-item-price {
  color: var(--brass-text);
}

/* --- Thumbnail grid cells ------------------------------------------------
   18 Aug 2026, paired with the align-items change in thumb_grid.php. Squaring the image cell
   is the other half of that fix: aligning the card tops stopped the rows staggering, but each
   framed mockup still has its own height (measured 189-300px across one search page), so the
   captions and prices underneath still started at seven different heights. A 1:1 cell with the
   mockup sitting on its bottom edge gives every frame a common ground line - like prints on a
   shelf - and puts every caption and price on the same baseline. Measured on derek: caption
   tops went from a 111px spread to 0.

   aspect-ratio rather than a fixed height so the cell scales with the grid at narrow widths.
   :has(img) so this only ever touches a cell that really is the picture - a grid variant whose
   first child is a caption is left alone, and on a browser without :has() the whole rule is
   dropped rather than misapplied.
   ------------------------------------------------------------------------ */
body.msh [id^="dmcsgrid"] > div > div:first-child:has(img) {
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: flex-end;
  justify-content: center;
}
body.msh [id^="dmcsgrid"] > div > div:first-child:has(img) img {
  max-height: 100%;
  max-width: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
}

/* --- Basket line note ----------------------------------------------------
   18 Aug 2026: order_detail.php's "Buy another and save" line. It used to be bold, sitting
   directly above an equally bold "Add Matching Greetings Card" - except that one is a link and
   this one is not, so the basket offered two identical-looking offers of which only the lower one
   did anything. This is information about what a second copy would save, so it now reads as a note
   and leaves the emphasis to the line that is actually clickable.
   ------------------------------------------------------------------------ */
body.msh .msh-basket-note {
  color: var(--ink-3);
  font-size: .875rem;
  margin: .35rem 0;
}

/* --- Basket lines opt out of the bare-link treatment ---------------------
   18 Aug 2026, same day as the rule above and because of it. A basket line's frame spec and its
   first breadcrumb crumb are bare anchors sitting in <p>, so the new prose rule underlined them -
   which put the emphasis on the specification and the word "Wall Art Prints" (a crumb that only
   points at the homepage) while the picture's own title, plain text, read as the least important
   thing in the row. Inverted hierarchy in the one place it matters most.

   A basket line is a component, not prose: Edit and Delete are its affordances and they are already
   right there. So it opts out, and the one thing in the row that genuinely is a call to action -
   the cross-sell, which carries .msh-inline-link - keeps its treatment because this only unsets
   class-less anchors.
   ------------------------------------------------------------------------ */
body.msh main .msh-basket-line a:not([class]) {
  color: inherit;
  text-decoration: none;
}

/* --- Pagination opts out of the bare-link treatment ----------------------
   The page numbers are class-less anchors sitting in <li> inside main, so the bare-link rule
   matched them and repainted the strip: accent-coloured, underlined numerals inside boxes
   that already carry their own border, background and hover. They are a control, not copy,
   and ul.pagination's own rule further up this file is the treatment they should have - this
   restores it. (The current page carries .currentpage, so it was never matched either way.)
   ------------------------------------------------------------------------ */
body.msh main ul.pagination a:not([class]) {
  color: var(--ink-2);
  text-decoration: none;
}
body.msh main ul.pagination a:not([class]):hover {
  color: var(--ink);
  text-decoration: none;
}

/* ==========================================================================
   Basket page restyle - 20 Aug 2026
   pages/basket.php + order_detail.php showBasket() render the basket from
   inline styles that both templates share, so this whole section works from
   class hooks those files now emit (.msh-basket-page on the wrapper,
   .msh-basket-line/.msh-basket-cols on the item cards, .msh-entry-row on
   showEntry()'s ledger rows, and so on). The hooks are inert without these rules -
   it has no rules for them, so it still renders from the inline styles alone.
   Everything here is scoped under .msh-basket-page, which ONLY basket.php
   emits: the same showBasket() markup on the thank-you/my-account order views
   keeps its current look. The !importants are not specificity fights with
   other CSS - they are the only way to beat the shared inline styles this
   markup carries, and each one is scoped to this page.
   ========================================================================== */

/* Item cards: white card on the paper ground instead of paper-2-on-paper
   (which barely read as a card at all), the standard card radius/shadow,
   and room to breathe - the 10px padding / 4px gaps were the single biggest
   "dated" signal on the page. */
body.msh .msh-basket-page .msh-basket-line {
  background: var(--card) !important;
  border: 1px solid var(--line) !important;
  border-radius: var(--radius);
  box-shadow: var(--sh-1);
  padding: 1.25rem !important;
  margin-bottom: .85rem !important;
}

/* Desktop item rows (.msh-basket-cols is only emitted on the IsMobile=false
   branch): grid instead of the inline display:flex. Grid ignores the
   children's inline flex:1/flex:3, which is exactly what makes this
   restyle possible without touching showBasket()'s column markup - fixed
   thumbnail and price columns, fluid details column. */
body.msh .msh-basket-page .msh-basket-cols {
  display: grid !important;
  grid-template-columns: 150px minmax(0,1fr) 100px 110px;
  gap: 0 1.5rem;
  align-items: start;
}

/* Thumbnail column: the mockup is already a framed print, so give it the
   floating-frame shadow the search grid uses, and keep it inside its cell. */
body.msh .msh-basket-page .msh-basket-cols > div:first-child img {
  max-width: 100%;
  height: auto;
  filter: drop-shadow(0 6px 12px rgba(22,19,15,.16));
}
body.msh .msh-basket-page .msh-basket-cols > div:first-child > p:first-child {
  margin-top: 0;
}

/* Edit / Delete under the thumbnail: quiet pill buttons instead of two bare
   ink links that sat flush against the picture. The bare-link opt-out above
   already stripped their underline; this gives them an affordance. */
body.msh .msh-basket-page .msh-basket-cols > div:first-child > div p {
  margin: .35rem 0;
}
body.msh .msh-basket-page .msh-basket-cols > div:first-child > div a {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: .82rem;
  white-space: nowrap;
  color: var(--ink-2);
  background: var(--paper);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: .18rem .7rem;
}
body.msh .msh-basket-page .msh-basket-cols > div:first-child > div a:hover {
  color: var(--ink);
  background: var(--card);
  border-color: var(--line-2);
}
body.msh .msh-basket-page .msh-basket-cols > div:first-child > div a .msh-icon {
  width: 15px;
  height: 15px;
}

/* Details column: tighten the paragraph rhythm (the sitewide 1em margins
   read as gaps between unrelated lines at this density), mute the copyright
   credit, and shrink the "Wall Art Prints > ..." breadcrumb to a caption -
   it's provenance, not a call to action, and at full body size it competed
   with the item title. :has() with the usual graceful-degradation caveat:
   without it the crumb line just stays body-sized, nothing breaks. */
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(2) p {
  margin: .35rem 0;
}
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(2) > p:first-child {
  margin-top: 0;
  font-size: 1.02rem;
  line-height: 1.45;
}
body.msh .msh-basket-page .msh-basket-line em {
  color: var(--ink-3);
  font-size: .875rem;
}
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(2) p:has(> span.plain) {
  font-size: .84rem;
}
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(2) p:has(> span.plain) a {
  color: var(--ink-3) !important;
}
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(2) p:has(> span.plain) span.plain {
  color: var(--ink-3);
}

/* Quantity / Price column headers: the bold body-size "Quantity"/"Price"
   repeated on every card read as content; as small caps labels they recede
   to what they are - column captions. The !important beats the qty label's
   inline font-weight:bold. */
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(3) > p:first-child,
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(4) > p:first-child {
  font-size: .72rem;
  font-weight: 600 !important;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0 0 .45rem;
}
body.msh .msh-basket-page .msh-basket-cols > div:nth-child(4) {
  text-align: right !important;
  font-size: 1.125rem;
}

/* Quantity input: the browser-default number field was the page's most
   obviously-2005 control. Sized to its two digits, themed to the palette. */
body.msh .msh-basket-page .msh-basket-cols input[type=number] {
  width: 64px;
  padding: .45rem .2rem;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  text-align: center;
}
body.msh .msh-basket-page .msh-basket-cols input[type=number]:focus {
  outline: none;
  border-color: var(--brass);
  box-shadow: 0 0 0 3px rgba(176,132,47,.14);
}

/* "No longer available" cards keep the card treatment but flag themselves. */
body.msh .msh-basket-page .msh-basket-warn {
  border-left: 3px solid var(--terracotta) !important;
}

/* showBasket()'s desktop Subtotal wrapper: on this page the summary reads as
   one right-hand ledger, so the first Subtotal row drops its card chrome and
   joins the ledger instead of sitting alone in a grey slab. */
body.msh .msh-basket-page .msh-basket-subtotal {
  background: none !important;
  border: 0 !important;
  padding: 0 !important;
  margin: 0 !important;
}

/* The ledger itself: every showEntry() row pulled into a consistent right-
   hand column with the amounts right-aligned under the card prices above,
   instead of amounts centered in a stray flex:1 that lined up with nothing. */
/* padding-right lines the ledger amounts up under the card price column
   above them (the cards inset their content by 1.25rem padding + 1px
   border); the checkout button below deliberately stays at the full right
   edge - it is a block, not a figure in the column of amounts. */
body.msh .msh-basket-page .msh-entry-row {
  max-width: 460px;
  margin-left: auto;
  padding: .45rem calc(1.25rem + 1px) .45rem 0 !important;
}
body.msh .msh-basket-page .msh-entry-row > div[style*='font-weight:bold'] {
  flex: 0 0 120px !important;
  text-align: right !important;
}
/* Caption-only rows (desktop: a flex:4 spacer + the caption): drop the
   spacer and right-align the caption with the amounts. It's the "Includes
   Free Shipping" line, so it takes the palette's positive tone. */
body.msh .msh-basket-page .msh-entry-row > div[style*='flex:4'] {
  display: none;
}
body.msh .msh-basket-page .msh-entry-row > div[style*='flex:4'] + div {
  flex: 1 !important;
  text-align: right !important;
  color: var(--sage);
  font-weight: 600;
}

/* Total: the one row that must not blend in - ruled off from the rows above
   like the foot of an invoice. The strong[style*='larger'] hook is the
   inline emphasis basket.php puts on exactly (and only) the Total row. */
body.msh .msh-basket-page .msh-basket-summary .msh-entry-row:has(strong[style*='larger']) {
  border-top: 2px solid var(--ink);
  margin-top: .5rem;
  padding-top: .8rem !important;
}

/* Continue Shopping / voucher / shipping-country band. The desktop markup
   is a float-left column plus a flex column inside an overflow:hidden box;
   as a flex container the float is ignored and the two halves centre on one
   axis instead of hanging at their own heights. (The mobile branch's panel
   has no second column, so the same rules just centre its stacked content
   vertically, which is a no-op.) */
body.msh .msh-basket-page .msh-basket-panel {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.25rem !important;
  margin: 1.25rem 0;
}
/* Only the desktop branch's panel (the one with the overflow:hidden float
   layout) becomes a flex row - the IsMobile branch's panel holds a different,
   stacked set of children and keeps its flow layout. */
body.msh .msh-basket-page .msh-basket-panel[style*='overflow'] {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}
body.msh .msh-basket-page .msh-basket-panel details {
  margin-top: .5rem;
}
body.msh .msh-basket-page .msh-basket-panel summary {
  font-size: .9rem;
  color: var(--link);
}
body.msh .msh-basket-page .msh-basket-panel input[type=text] {
  padding: .45rem .6rem;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
}

/* Arrival estimate: sits directly under the Total, aligned with the ledger,
   in the palette's positive tone - it's the reassurance line of the page. */
body.msh .msh-basket-page .msh-basket-eta {
  max-width: 460px;
  margin: .3rem 0 1rem auto;
  padding-right: calc(1.25rem + 1px);
  text-align: right;
  color: var(--sage);
  font-weight: 600;
}

/* Checkout column: capped to the same width as the ledger so the button,
   card logos and trust lines form one aligned block, and the primary action
   fills that block's width instead of floating at its natural size. */
body.msh .msh-basket-page .msh-basket-actions {
  max-width: 460px;
  margin-left: auto;
}
body.msh .msh-basket-page .msh-basket-actions .dmcsButton {
  width: 100%;
}
body.msh .msh-basket-page .msh-basket-actions p:has(.msh-icon) {
  color: var(--ink-3);
  font-size: .9rem;
  margin: .35rem 0;
}

/* Empty basket: a centered moment rather than two headings hugging the left
   margin. */
body.msh .msh-basket-page.msh-basket-empty {
  text-align: center;
  padding: 2.5rem 0 1rem;
}
body.msh .msh-basket-page.msh-basket-empty h2 {
  font-weight: 400;
  font-size: 1.5rem;
  color: var(--ink-2);
  margin: 0 0 1.5rem;
}

/* Narrow viewports still serving the desktop markup (IsMobile is a UA
   decision, not a width one): collapse the item grid to thumbnail + content,
   with quantity and price flowing under the details, and let the ledger and
   checkout block take the full width. */
@media (max-width: 760px) {
  body.msh .msh-basket-page .msh-basket-cols {
    grid-template-columns: 96px minmax(0,1fr);
    gap: 0 1rem;
  }
  body.msh .msh-basket-page .msh-basket-cols > div:first-child {
    grid-row: span 3;
  }
  body.msh .msh-basket-page .msh-basket-cols > div:nth-child(3),
  body.msh .msh-basket-page .msh-basket-cols > div:nth-child(4) {
    grid-column: 2;
    text-align: left !important;
  }
  body.msh .msh-basket-page .msh-entry-row,
  body.msh .msh-basket-page .msh-basket-eta,
  body.msh .msh-basket-page .msh-basket-actions {
    max-width: none;
  }
}

/* ==========================================================================
   Print Your Own Photos - 20 Aug 2026
   pages/custom.php (?page=custom / custom.html) under the msh redesign: hero
   intro, numbered how-it-works row, the pages/upload.php uploader restyled as
   a card, and the privacy/retention promises as a .msh-trust strip. The page
   emits the .msh-pyop* hooks. upload.php itself is
   untouched - its markup is shared by every other upload flow (Add Media,
   the photographer job tools) - so everything below reaches into it from the
   .msh-pyop-upload wrapper only this page puts around it. The !importants
   beat that file's inline widths/paddings, nothing else.
   ========================================================================== */

/* Tagline: keep the h1+h2 semantics the page has always had, but render the
   h2 as a subtitle instead of a second full-size display heading - the two
   stacked banner headings were the page's loudest "dated" signal. */
body.msh .msh-pyop-tagline {
  font-size: clamp(1.25rem, 2.4vw, 1.75rem);
  font-weight: 400;
  color: var(--ink-2);
  margin: .75rem 0 .5rem;
}
body.msh .msh-pyop-hero .msh-lede { margin-top: .25rem; }

/* How-it-works: three numbered cards. auto-fit so they stack cleanly on
   phones without a media query. */
body.msh .msh-pyop-steps {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: clamp(.75rem, 1.5vw, 1.25rem);
  margin: clamp(1.5rem, 3vw, 2.25rem) 0;
}
body.msh .msh-pyop-steps > div {
  display: flex;
  gap: .9rem;
  align-items: flex-start;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--sh-1);
  padding: 1.1rem 1.25rem;
}
body.msh .msh-pyop-stepnum {
  flex: 0 0 auto;
  width: 2rem;
  height: 2rem;
  display: grid;
  place-items: center;
  border: 1px solid var(--brass);
  border-radius: 999px;
  color: var(--brass-text);
  font-family: var(--display);
  font-size: 1.05rem;
}
body.msh .msh-pyop-steps b { display: block; font-size: .95rem; margin-bottom: .15rem; }
body.msh .msh-pyop-steps span:not(.msh-pyop-stepnum) { font-size: .85rem; color: var(--ink-3); }

/* "Your uploaded photos" header row with the Add More Photos toggle. */
body.msh .msh-pyop-toolbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  margin: 2.25rem 0 1rem;
}
body.msh .msh-pyop-toolbar .msh-pyop-h2 {
  font-size: clamp(1.3rem, 2.4vw, 1.8rem);
  margin: 0;
}

/* The uploader card around pages/upload.php's shared markup. The legacy
   stylesheet caps .dmcsForm at a fixed width (measured 445px here), which
   squeezed the drop zone into a letterbox - inside this card the form takes
   the card's width. */
body.msh .msh-pyop-upload form.dmcsForm {
  width: 100% !important;
  max-width: none !important;
}
body.msh .msh-pyop-upload {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--sh-1);
  padding: clamp(1.25rem, 3vw, 2rem);
  margin: 1.25rem 0 0;
}

/* Two-column layout inside the card: drop zone and queue on the left,
   "How would you like it printed?" and the upload CTA on the right. The
   form's children are upload.php's fixed structure (#exampleContainer,
   #filelist, #clearButtonContainer, #overallProgress, then $PrintOption's
   <p>s and #uploadFilesButtonContainer), so each left-hand block gets an
   explicit row and the right-hand column auto-places alongside. Hidden
   per-product-type selects are display:none and never join the grid. */
body.msh .msh-pyop-upload form.dmcsForm {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  /* Rows 1-4: select button / heading, type picker, product picker, Begin
     Upload. Row 5 is the flexible filler that absorbs the drop zone's extra
     height, so the right column stays compact at the top. */
  grid-template-rows: auto auto auto auto 1fr auto auto;
  column-gap: clamp(1.5rem, 3vw, 2.5rem);
  align-items: start;
}
body.msh .msh-pyop-upload form.dmcsForm > #exampleContainer { grid-column: 1; grid-row: 1; }
/* The drop zone spans rows 2-5; the 1fr filler row takes all of its extra
   height - a taller drop target, not taller gaps between the pickers. */
body.msh .msh-pyop-upload form.dmcsForm > #filelist {
  grid-column: 1;
  grid-row: 2 / 6;
  align-self: stretch;
  min-height: 220px;
  display: grid;
  align-content: center;
}
body.msh .msh-pyop-upload form.dmcsForm > #clearButtonContainer { grid-column: 1; grid-row: 6; }
body.msh .msh-pyop-upload form.dmcsForm > #overallProgress { grid-column: 1; grid-row: 7; }
body.msh .msh-pyop-upload form.dmcsForm > p,
body.msh .msh-pyop-upload form.dmcsForm > #uploadFilesButtonContainer { grid-column: 2; }
body.msh .msh-pyop-upload form.dmcsForm > br { display: none; }
/* upload.php emits one <p><select id='ProductType_N'> per product type and
   JS-toggles the SELECTs (not the <p>s), so ~20 empty paragraphs would each
   claim a grid row between the picker and the Begin Upload button. Drop the
   wrappers whose select is hidden - exactly one stays visible at a time, so
   the right column is always heading / type / product / CTA. Matches both
   the PHP-emitted "display:none" and the JS-set "display: none" spellings;
   without :has() support the phantom rows return but nothing breaks. */
body.msh .msh-pyop-upload form.dmcsForm > p:has(> select[id^='ProductType_'][style*='none']) { display: none; }
@media (max-width: 760px) {
  body.msh .msh-pyop-upload form.dmcsForm { display: block; }
}

/* File list: framed as the drop target it actually is (the YUI uploader's
   dragAndDropArea is the whole body, so this is signposting, not a hitbox). */
body.msh .msh-pyop-upload #filelist {
  border: 1.5px dashed var(--line-2);
  border-radius: var(--radius);
  background: var(--paper);
  padding: .75rem 1rem;
  margin: 1rem 0;
}
body.msh .msh-pyop-upload #filenames { width: 100%; border-collapse: collapse; }
/* Empty state: while the "No images selected" row (#nofiles) is present the
   File Name / File Size / Status headers label nothing - hide them until the
   uploader's fileselect handler removes #nofiles. Without :has() the headers
   just show over the empty state, as before. */
body.msh .msh-pyop-upload #filenames:has(#nofiles) thead tr:first-child { display: none; }
body.msh .msh-pyop-upload #filenames th {
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
  text-align: left;
  padding: .4rem .75rem !important;
}
body.msh .msh-pyop-upload #filenames td { padding: .5rem .75rem !important; }
body.msh .msh-pyop-upload #ddmessage {
  text-align: center;
  color: var(--ink-3);
  padding: 1.75rem 1rem !important;
}
body.msh .msh-pyop-upload #ddmessage strong { font-weight: 500; }
/* Drag-over highlight: upload.php's own .yellowBackground, retoned to the
   palette. Higher specificity beats its inline <style> block, no !important
   needed. */
body.msh .msh-pyop-upload .yellowBackground { background: var(--brass-soft); color: var(--ink); }

/* Reset: demote from a 200px slab to a quiet ghost button - it clears the
   queue, it should not weigh the same as the two real steps. */
body.msh .msh-pyop-upload #clearButton {
  width: auto !important;
  height: auto !important;
  padding: .45rem 1.1rem;
  text-transform: capitalize;
}
body.msh .msh-pyop-upload #overallProgress { color: var(--ink-3); font-size: .9rem; }

/* Product pickers ("How would you like it printed?"): themed to match
   .msh-input instead of browser-default selects. */
body.msh .msh-pyop-upload form > p > strong { font-size: 1rem; }
body.msh .msh-pyop-upload form > p > select {
  min-width: min(300px, 100%);
  max-width: 100%;
  padding: .55rem .75rem;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
}
body.msh .msh-pyop-upload form > p > select:focus {
  outline: none;
  border-color: var(--brass);
  box-shadow: 0 0 0 3px rgba(176,132,47,.14);
}

/* Promise strip: .msh-trust supplies the grid; this just tones the icons and
   spaces it off the content above. */
body.msh .msh-pyop-trust { margin-top: clamp(2rem, 4vw, 3rem); }
body.msh .msh-pyop-trust .msh-icon { flex: 0 0 auto; color: var(--brass); }

/* --- Legacy content pages (20 Aug 2026) ----------------------------------
   index.php's dispatch site wraps its whitelisted "plain content" pages
   (the [data-reveal] entrance list: policies, help/FAQ, vouchers, requests,
   subscribe, partners, blog, findimage...) in .msh-content, so ~20 pages
   that mostly echo admin-authored HTML straight from the DB pick up one
   shared treatment here instead of each growing its own markup fork.
   .msh-content--prose additionally narrows the pure reading/form pages to a
   centred column; grid/media pages (partners, blog, findimage) stay at the
   full measure. Everything below is defensive about the content it styles:
   the HTML inside is whatever an admin typed into policy_text/webpage rows,
   so rules target bare elements, assume nothing carries a class, and cap
   rather than resize (the About Us row, for one, ships a hardcoded
   width:1400px layout table - hence the !important width cap, inline style
   would beat anything weaker). */
body.msh .msh-content {
  padding-block: .75rem 4rem;
  /* .dmcsDivMid only carries its legacy inline 10px; bring these pages up
     to the sitewide gutter. */
  padding-inline: calc(var(--gutter) - 10px);
}
body.msh .msh-content--prose {
  max-width: 62rem;
  margin-inline: auto;
  box-sizing: content-box;   /* 62rem of text regardless of the gutter */
}

/* Page title: the same centred display treatment the curated listing pages
   carry (see the editors-picks/trending block further up) - these pages'
   h1s come out of the DB bare, so the class hook used there does not
   exist here. */
body.msh .msh-content h1 {
  text-align: center;
  text-wrap: balance;
  margin-top: 2.25rem;
}
body.msh .msh-content h1::after {
  content: "";
  display: block;
  width: 56px;
  height: 3px;
  margin: 1.1rem auto 0;
  border-radius: 2px;
  background: var(--brass);
}
@media (max-width: 700px) {
  body.msh .msh-content h1 { margin-top: 1.25rem; }
}

/* An h2 straight after the h1 is a strapline (vouchers, several policy
   rows), not a section heading - same taming the curated pages apply.
   Reading-column pages only: on the wide pages (partners) the first h2 is
   a real section heading, not a strapline. */
body.msh .msh-content--prose h1 + h2 {
  font-family: var(--sans);
  font-size: 1.125rem;
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: .01em;
  color: var(--ink-2);
  max-width: 46rem;
  margin: 1rem auto 0;
  text-align: center;
  text-wrap: balance;
}
/* ...and the first paragraph of the page reads as a centred lede. Only the
   one directly under the title run - body copy below stays left-set. */
body.msh .msh-content--prose h1 + p,
body.msh .msh-content--prose h1 + h2 + p {
  text-align: center;
  color: var(--ink-3);
  font-size: 1.0625rem;
  line-height: 1.6;
  max-width: 46rem;
  margin: 1.1rem auto 2rem;
}

/* Section headings inside the content: keep the display face but step well
   down from the title - the sitewide h2 clamp reads as a second h1 at
   these lengths. */
body.msh .msh-content h2 {
  font-size: clamp(1.3rem, 2.4vw, 1.65rem);
  margin-top: 2.3rem;
  margin-bottom: .6rem;
}
body.msh .msh-content h3 {
  margin-top: 1.8rem;
  margin-bottom: .5rem;
}

/* Prose rhythm. No measure cap on the text itself - the --prose wrapper
   caps the whole block, headings included, so there is a single edge (see
   the "No measure cap on body copy" note near the top of this file). */
body.msh .msh-content p,
body.msh .msh-content li {
  line-height: 1.7;
}
body.msh .msh-content li { margin-block: .3rem; }
body.msh .msh-content hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin-block: 2.5rem;
}
body.msh .msh-content--prose img { max-width: 100%; height: auto; }
body.msh .msh-content--prose table {
  width: 100% !important;   /* admin rows carry inline width:1400px */
  max-width: 100%;
  border-collapse: collapse;
  /* Auto table layout treats width as a floor and re-expands to the admin
     markup's min-content (measured 1400px on the About Us row), so the cap
     above only sticks under fixed layout. Cells share the row evenly and
     long tokens wrap instead of punching the column open. */
  table-layout: fixed;
}
body.msh .msh-content--prose td { overflow-wrap: break-word; }

/* Form controls on these pages get the same rounded/padded field the
   sign-in and account cards use (.msh-input look, applied by element since
   none of this older markup carries classes). */
body.msh .msh-content input[type="text"],
body.msh .msh-content input[type="email"],
body.msh .msh-content input[type="password"],
body.msh .msh-content input[type="number"],
body.msh .msh-content select,
body.msh .msh-content textarea {
  padding: 9px 12px;
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  box-sizing: border-box;
  max-width: 100%;
}
body.msh .msh-content input[type="text"]:focus,
body.msh .msh-content input[type="email"]:focus,
body.msh .msh-content input[type="password"]:focus,
body.msh .msh-content input[type="number"]:focus,
body.msh .msh-content select:focus,
body.msh .msh-content textarea:focus {
  outline: none;
  border-color: var(--ink);
}

/* --- FAQ accordion (pages/help.php) --------------------------------------
   help.php's own inline <style> block (yellow #fece0a headers) is skipped
   - these rules carry the whole look. Markup and the
   click-toggle JS are unchanged: .accordion > .accordion-item >
   .accordion-header + .accordion-content, one .accordion wrapper per Q. */
body.msh .msh-content .accordion {
  max-width: none;
  margin: 0 0 .65rem;
}
body.msh .msh-content .accordion-item {
  margin-bottom: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  overflow: hidden;
  box-shadow: var(--sh-1);
}
body.msh .msh-content .accordion-header {
  position: relative;
  padding: .85rem 3rem .85rem 1.1rem;
  background: var(--card);
  color: var(--ink);
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
}
body.msh .msh-content .accordion-header:hover,
body.msh .msh-content .accordion-header.active {
  background: var(--paper-2);
  color: var(--ink);
}
body.msh .msh-content .accordion-header::after {
  content: "";
  position: absolute;
  right: 1.2rem;
  top: 50%;
  width: .5em;
  height: .5em;
  margin-top: -.4em;
  border-right: 2px solid var(--brass);
  border-bottom: 2px solid var(--brass);
  transform: rotate(45deg);
  transition: transform .2s var(--ease);
}
body.msh .msh-content .accordion-header.active::after {
  transform: rotate(225deg);
  margin-top: -.1em;
}
body.msh .msh-content .accordion-content {
  display: none;   /* collapsed until the toggle JS sets display:block */
  padding: .15rem 1.1rem 1rem;
  background: var(--card);
  color: var(--ink-2);
  line-height: 1.65;
}

/* --- Reviews page (pages/testimonials.php) -------------------------------
   Each review is a bare <p><strong>rating + name</strong></p> followed by a
   <blockquote> holding the comment; the card goes on the quote. */
body.msh[data-page="reviews"] .msh-content blockquote {
  margin: .4rem 0 1.6rem;
  padding: .9rem 1.25rem;
  background: var(--card);
  border: 1px solid var(--line);
  border-left: 3px solid var(--brass);
  border-radius: var(--radius);
  box-shadow: var(--sh-1);
}
body.msh[data-page="reviews"] .msh-content blockquote p { margin-block: .35rem; }

/* --- Payment step (pages/secure_payment_stripe.php + _braintree.php) -----
   The last page a paying customer sees was the last one still on legacy
   styling: css/stripe.css paints the submit button and spinner #fece0a
   yellow with an unscoped `button{}` rule, and the Braintree fragment is
   built from inline greys (retokened in the PHP itself - see $_BTCARD et
   al. there). Everything here outranks stripe.css's bare element selectors
   by specificity, so load order doesn't matter; the two !importants beat
   inline styles the markup carries. */
body.msh .msh-pay {
  gap: clamp(1.25rem, 3vw, 3rem);
  align-items: flex-start;
  padding-block: .5rem 2rem;
}
body.msh .msh-pay .flex-item-left,
body.msh .msh-pay .flex-item-right {
  flex: 1 1 400px;
  max-width: 520px;
  padding: 0;
}
body.msh .msh-pay .stripe { display: block; }
body.msh .msh-pay form#payment-form {
  width: auto;
  min-width: 0;
  background: var(--card);
  padding: clamp(1.25rem, 3vw, 2rem);
}
body.msh .msh-pay form#payment-form h2 {
  font-size: 1.35rem;
  margin: 0 0 1.1rem;
}
body.msh .msh-pay button#submit {
  background: var(--btn);
  color: var(--btn-text);
  border: 1px solid var(--btn);
  border-radius: var(--radius) !important;   /* inline 50px pill on the tag */
  width: 100%;
  padding: .85rem 1.5rem;
  font-family: var(--sans);
  font-weight: 600;
  letter-spacing: .02em;
  box-shadow: none;
  transition: background .15s var(--ease);
}
body.msh .msh-pay button#submit:hover {
  background: var(--btn-hover);
  border-color: var(--btn-hover);
  filter: none;
}
body.msh .msh-pay button#submit:disabled { opacity: .55; }
/* The loading spinner sits inside the submit button: its wedges must match
   the button fill (they sweep over the inset ring to draw the arc), and the
   ring itself takes the button's text colour. */
body.msh .msh-pay .spinner { color: var(--btn-text); }
body.msh .msh-pay .spinner::before,
body.msh .msh-pay .spinner::after { background: var(--btn); color: var(--btn-text); }
body.msh .msh-pay #payment-message {
  color: var(--terracotta);
  font-size: 1rem;
  line-height: 1.4;
  background: transparent !important;   /* inline #fff on the tag */
}
body.msh .msh-pay p img[src*='cards_accepted'] { opacity: .85; }

/* Braintree variant (data-page covers both fragments; .msh-pay only exists
   on the Stripe one). Typography only - the box chrome is retokened inline
   in the PHP, and the Drop-in widget styles itself. */
body.msh[data-page="secure-payment"] main h1 {
  font-size: clamp(1.5rem, 2.6vw, 2rem);
  margin-block: 1.5rem .5rem;
}

/* --- Legacy transactional and account-tool pages (20 Aug 2026) -----------
   Order confirmation/tracking, the printable receipt's on-site chrome, the
   lightbox/membership/media-workshop tools, promotions, unsubscribe. These
   stay on their legacy PHP markup (order_recipt.php's tables double as
   email/print paperwork, so their internals are deliberately untouched);
   what the redesign owes them is the page frame: the shared 1440px measure
   with real gutters instead of .dmcsDivMid's inline 10px, and heading
   scale in step with the rest of the site. */
body.msh:is([data-page="thankyou"],[data-page="order-details"],[data-page="order-receipt"],[data-page="cart-details"],[data-page="my-lightbox"],[data-page="membership"],[data-page="media-workshop"],[data-page="build-a-frame"],[data-page="promotion"],[data-page="unsubscribe"],[data-page="composition"],[data-page="add-item"],[data-page="add-digital"],[data-page="secure-payment"]) .dmcsDivMid {
  max-width: var(--maxw);
  margin-inline: auto;
  box-sizing: border-box;
  padding-left: var(--gutter) !important;    /* inline 10px on the tag */
  padding-right: var(--gutter) !important;
  padding-bottom: 3rem;
}
body.msh:is([data-page="thankyou"],[data-page="order-details"],[data-page="order-receipt"],[data-page="cart-details"],[data-page="my-lightbox"],[data-page="membership"],[data-page="media-workshop"],[data-page="build-a-frame"],[data-page="promotion"],[data-page="unsubscribe"]) .dmcsDivMid h1 {
  font-size: clamp(1.6rem, 2.8vw, 2.25rem);
  margin-block: 1.5rem .75rem;
}
body.msh:is([data-page="thankyou"],[data-page="order-details"],[data-page="order-receipt"],[data-page="cart-details"],[data-page="my-lightbox"],[data-page="membership"],[data-page="media-workshop"],[data-page="build-a-frame"],[data-page="promotion"],[data-page="unsubscribe"]) .dmcsDivMid h2 {
  font-size: clamp(1.2rem, 2vw, 1.5rem);
}

/* Bordered tables from css/stylesheet.php (which paints them #888): soften
   to the palette's line tokens. */
body.msh #dmcsBordered,
body.msh #dmcsBordered TD,
body.msh #dmcsBordered TH {
  border: 1px solid var(--line);
  padding: 8px 10px;
}
body.msh #dmcsBordered TH { background: var(--paper-2); }
