/* ---------------------------------------------------------------- typefaces */
/*
   Self-hosted rather than pulled from Google. Fetching them from fonts.gstatic
   cost a 1,725ms critical path: the browser had to parse the HTML, request a
   stylesheet from fonts.googleapis, parse THAT, and only then discover the
   woff2 files on a third origin — two extra DNS lookups and TLS handshakes
   before a single glyph was on the wire. Served from our own origin the files
   are preloaded straight from the <head> and start downloading immediately.

   Two families, four faces. Kiona — roman and italic — is the display voice;
   Inter is the interface voice, still split latin / latin-ext on Google's own
   unicode-range values so latin-ext only downloads when a page actually uses
   an Eastern European character.

   KIONA HAS NO LOWERCASE. All twenty-six lowercase glyphs in the file are the
   capitals again, with identical outlines and widths. That is a property of
   the typeface, not a bug here: every heading on the site therefore sets in
   capitals whatever case the markup is written in. It is why --font-body below
   stays on Inter, and why nothing routes body copy, prices, buttons or the
   checkout through --font-display.
   =========================================================================== */

@font-face {
    font-family: 'Kiona';
    font-style: normal;
    font-weight: 100 900;
    font-display: swap;
    src: url(../fonts/kiona.woff2) format("woff2");
}

/* Kiona ships a drawn italic as its own file, so the three places that set the
   display face in italic — .hero-art__title em, .artist__name and
   .artist__stat-value — get a real one instead of a mechanical slant. */
@font-face {
    font-family: 'Kiona';
    font-style: italic;
    font-weight: 100 900;
    font-display: swap;
    src: url(../fonts/kiona-italic.woff2) format("woff2");
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 300 600;
    font-display: swap;
    src: url(../fonts/inter-latin-ext.woff2) format("woff2");
    unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
    font-family: 'Inter';
    font-style: normal;
    font-weight: 300 600;
    font-display: swap;
    src: url(../fonts/inter-latin.woff2) format("woff2");
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ===========================================================================
   BLVCK MILK — storefront design system
   ---------------------------------------------------------------------------
   Written from scratch rather than layered over a framework, because the point
   of the whole design is that it should not read as a generic shop. The rules
   below are deliberately few: a paper-white ground, one serif for voice, one
   sans for interface, and a lot of restraint about everything else.

   The artwork is the only thing on the page allowed to be loud.
   =========================================================================== */

/* ---------------------------------------------------------------- tokens */
/*
   The gallery runs dark. Every colour in the stylesheet resolves through the
   tokens below, so the whole site is one palette rather than a light design
   with a dark skin bolted on.
                       .
   Two things drive the values. First, artwork: a true black ground makes a
   dark painting look like a hole in the page and crushes the shadow detail in
   a photograph, so the ground is a very dark warm grey and each step up is
   small. Second, text: pure white on near-black vibrates and is tiring to
   read, so the brightest ink stops short of #fff.

   The `--paper` / `--ink` names are kept even though they now mean the
   opposite of what they say on paper. They are used in about 900 places and
   they still describe the RELATIONSHIP correctly — paper is what a thing sits
   on, ink is what is written on it — which is the part that matters.
*/
:root {
    color-scheme: dark;

    /* Ground, and the two steps above it. The gaps are deliberately narrow:
       a card that jumps too far off the page competes with the art on it. */
    --paper: #101012;
    --paper-raised: #17171b;
    --paper-sunken: #1c1c21;

    /* Ink, brightest first. --ink stops short of white on purpose. */
    --ink: #f2f0ec;
    --ink-soft: #b4b0a8;
    --ink-faint: #837f78;

    /* Hairlines have to survive on a dark ground without becoming stripes. */
    --rule: #2a2a30;
    --rule-strong: #3a3a42;

    /* The same gold, lifted. At #8a6a3b it went muddy against this ground and
       failed contrast on small text; this reads as the same colour. */
    --accent: #c9a46a;
    --accent-deep: #e0bd88;
    --accent-wash: #241d13;

    /* Both lifted for the same reason as the accent. */
    --danger: #e0705f;
    --success: #6fae82;

    /* A dark block on a dark page cannot be `--ink` any more. These are the
       two surfaces that deliberately inverted in the light design — the
       footer and the ink section — and they now go DOWN instead of up. */
    --ink-surface: #08080a;
    --ink-surface-text: rgba(242, 240, 236, 0.72);
    /* Headings and controls on that surface. It has to be a token of its own:
       the surface inverts with the theme but --paper and --ink do not follow it,
       so anything painted with those lands right in one theme and vanishes in
       the other. */
    --ink-surface-strong: #f2f0ec;
    /* The accent as it has to look ON that surface. --accent is tuned against
       --paper; on the ink surface the light theme's #8a6a3b falls to about
       1.6:1 and the link may as well not be there. */
    --ink-surface-accent: #e0bd88;

    /* Selected text. Stated as a token because the highlight has to carry
       against whichever ground is underneath it, and the two grounds are
       opposites. */
    --selection-bg: rgba(201, 164, 106, 0.30);

    --font-display: 'Kiona', 'Futura', 'Avenir Next', 'Century Gothic',
        'Helvetica Neue', Arial, sans-serif;
    --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI',
        Roboto, 'Helvetica Neue', Arial, sans-serif;

    /* Header height is a token because sticky panels, the flash stack and
       scroll-margin all have to agree with it — and it shrinks on phones. */
    --header-h: 76px;

    --measure: 68ch;
    --shell: 1440px;
    --gutter: clamp(1.25rem, 4vw, 4rem);

    --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
    --slow: 620ms;
    --quick: 220ms;

    /* Shadow does almost no work on a dark ground — there is nothing for it to
       darken. These stay for depth under raised panels, but separation is
       carried by --rule and by the small steps in the paper scale instead. */
    --shadow-lift: 0 2px 4px rgba(0, 0, 0, 0.30),
        0 12px 32px rgba(0, 0, 0, 0.45);
    --shadow-frame: 0 1px 2px rgba(0, 0, 0, 0.40),
        0 18px 48px rgba(0, 0, 0, 0.60);

    /* Artwork is the one thing allowed to keep its own light, so a frame gets
       a soft rounded corner and the mat behind it a faint lift. */
    --radius-art: 20px;
    --radius-control: 10px;

    /* The hero sets type over a painting. This is the ground colour of the
       scrim that makes that legible, as raw channels so the gradients can
       vary only the alpha — and it has to follow the theme, or a light page
       ends up laying a black veil over the art. */
    --hero-scrim: 16, 16, 18;
}

/*
   The original paper palette, kept whole so the site can be put back into
   daylight with one attribute on <html> and no other edit:
       <html data-theme="light">
*/
:root[data-theme="light"] {
    color-scheme: light;

    --paper: #faf9f7;
    --paper-raised: #ffffff;
    --paper-sunken: #f2f0ec;
    --ink: #171613;
    --ink-soft: #4a4741;
    --ink-faint: #8b867c;
    --rule: #e4e1da;
    --rule-strong: #cfcbc1;
    --accent: #8a6a3b;
    --accent-deep: #6b5129;
    --accent-wash: #f4efe6;
    --danger: #a13a2c;
    --success: #3f6b4a;
    --ink-surface: #171613;
    --ink-surface-text: rgba(250, 249, 247, 0.72);
    --ink-surface-strong: #faf9f7;
    --ink-surface-accent: #c9a46a;

    --selection-bg: rgba(138, 106, 59, 0.22);

    --shadow-lift: 0 2px 4px rgba(23, 22, 19, 0.03),
        0 12px 32px rgba(23, 22, 19, 0.07);
    --shadow-frame: 0 1px 2px rgba(23, 22, 19, 0.10),
        0 18px 48px rgba(23, 22, 19, 0.14);

    --hero-scrim: 250, 249, 247;
}

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

html {
    -webkit-text-size-adjust: 100%;
    scroll-behavior: smooth;
    /* Sideways containment belongs on the root, not on <body> — see the note on
       body below for what it was costing there. `clip` and not `hidden`: this
       sheet has six position: sticky elements, and `hidden` would make the root
       a scroll container and put them at risk, while `clip` never creates one.
       Safari picked clip up in 16; older versions simply get no containment,
       which is the trade for not breaking sticky on the pages that use it. */
    overflow-x: clip;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}

body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 16px;
    line-height: 1.65;
    font-feature-settings: 'kern' 1, 'liga' 1;
    -webkit-font-smoothing: antialiased;
    text-rendering: optimizeLegibility;
    /* NO overflow-x here — it moved to html above. The two axes cannot disagree
       about being visible: `overflow-x: hidden` here silently computed
       `overflow-y: auto`, making <body> a second scroll container nested inside
       the viewport's own. That is what makes vertical scrolling feel wrong on a
       phone — momentum hands off between the two containers mid-flick, the URL
       bar stops collapsing because the document is no longer what scrolls, and
       fixed elements jitter as the inner one settles. It was also the wrong
       element for the job: overflow-x on <body> is precisely what iOS Safari
       ignores when it comes to actually containing anything. */
}

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

a {
    color: inherit;
    text-decoration: none;
}

/* A visible focus ring is not optional — it is how a keyboard user navigates. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
    border-radius: 2px;
}

::selection {
    /* The gold carries the highlight in both themes, at the alpha each ground
       needs — see --selection-bg. A fixed value works on one ground only. */
    background: var(--selection-bg);
    color: var(--ink);
}

/*
   Chrome paints its own near-white block behind an autofilled field and
   ignores `background`, which on a dark form leaves a bright rectangle with
   invisible text in it. The inset shadow is the only way to repaint it, and
   the long transition stops Chrome from flashing the default on focus.
*/
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
select:-webkit-autofill {
    -webkit-text-fill-color: var(--ink);
    -webkit-box-shadow: 0 0 0 1000px var(--paper-raised) inset;
    box-shadow: 0 0 0 1000px var(--paper-raised) inset;
    caret-color: var(--ink);
    transition: background-color 100000s ease-in-out 0s;
}

/* A native date/time picker draws its indicator in black by default. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator {
    filter: invert(0.8);
    cursor: pointer;
}

/* ------------------------------------------------------------ typography */
h1,
h2,
h3,
h4,
.display {
    font-family: var(--font-display);
    font-weight: 400;
    line-height: 1.1;
    letter-spacing: -0.015em;
    margin: 0 0 0.5em;
    color: var(--ink);
}

h1,
.h1 {
    font-size: clamp(2.5rem, 5.5vw, 4.5rem);
}

h2,
.h2 {
    font-size: clamp(1.9rem, 3.6vw, 3rem);
}

h3,
.h3 {
    font-size: clamp(1.35rem, 2.2vw, 1.9rem);
}

h4,
.h4 {
    font-size: 1.15rem;
}

p {
    margin: 0 0 1.15em;
    max-width: var(--measure);
}

p:last-child {
    margin-bottom: 0;
}

/* The small-caps label that runs above almost every section on the site. */
.eyebrow {
    font-family: var(--font-body);
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin: 0 0 1rem;
    display: block;
}

.lede {
    font-size: clamp(1.05rem, 1.6vw, 1.25rem);
    color: var(--ink-soft);
    line-height: 1.7;
}

.muted {
    color: var(--ink-faint);
}

.small {
    font-size: 0.8125rem;
}

/* --------------------------------------------------------------- layout */
.shell {
    width: 100%;
    max-width: var(--shell);
    margin-inline: auto;
    /* viewport-fit=cover lets the page run under a notch in landscape, so the
       gutter has to grow by whatever the device reserves on each side. */
    padding-left: calc(var(--gutter) + env(safe-area-inset-left));
    padding-right: calc(var(--gutter) + env(safe-area-inset-right));
}

.shell--narrow {
    max-width: 920px;
}

.shell--reading {
    max-width: 720px;
}

.section {
    padding-block: clamp(3.5rem, 8vw, 7rem);
}

.section--tight {
    padding-block: clamp(2.5rem, 5vw, 4rem);
}

.section--sunken {
    background: var(--paper-sunken);
}

/* An inverted band: dark in the light theme, darker still in the dark one.
   Everything on it takes an ink-surface token, never --ink — under light,
   --ink-surface and --ink are the same value and the block went solid black
   with invisible text on it. */
.section--ink {
    background: var(--ink-surface);
    color: var(--ink-surface-text);
}

.section--ink h1,
.section--ink h2,
.section--ink h3 {
    color: var(--ink-surface-strong);
}

.section--ink .eyebrow {
    color: var(--ink-surface-text);
}

.section-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 2rem;
    flex-wrap: wrap;
    margin-bottom: clamp(2rem, 4vw, 3.25rem);
}

.section-head p {
    margin-bottom: 0;
}

/* The <hr> that breaks the mobile menu into two groups — catalogue above,
   account and actions below. Its hairline went with the rest of the section
   rules, so the break is carried by space instead. Without either it is one
   undifferentiated list of nine links. */
.rule {
    border: 0;
    margin: 0.9rem 0;
}

/* --------------------------------------------------------------- buttons */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    font-family: var(--font-body);
    font-size: 0.78rem;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    padding: 0.95rem 2rem;
    border: 1px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    cursor: pointer;
    transition: background var(--quick) var(--ease),
        color var(--quick) var(--ease),
        border-color var(--quick) var(--ease),
        transform var(--quick) var(--ease);
}

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

.btn:active {
    transform: translateY(1px);
}

.btn[disabled],
.btn.is-disabled {
    background: var(--rule-strong);
    border-color: var(--rule-strong);
    color: var(--paper);
    cursor: not-allowed;
    pointer-events: none;
}

.btn--ghost {
    background: transparent;
    color: var(--ink);
}

.btn--ghost:hover {
    background: var(--ink);
    color: var(--paper);
}

.btn--light {
    background: var(--paper);
    border-color: var(--paper);
    color: var(--ink);
}

.btn--light:hover {
    background: transparent;
    color: var(--paper);
    border-color: var(--paper);
}

.btn--small {
    padding: 0.65rem 1.35rem;
    font-size: 0.7rem;
}

.btn--wide {
    width: 100%;
}

/* A text link that reads as an editorial "read more". */
.link-underline {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.78rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    padding-bottom: 2px;
    border-bottom: 1px solid var(--rule-strong);
    transition: border-color var(--quick) var(--ease), color var(--quick) var(--ease);
}

.link-underline:hover {
    border-color: var(--ink);
    color: var(--accent-deep);
}

/*
 * Between the point the burger disappears (961px) and the width the five
 * destinations comfortably fit, the nav has to give ground or it pushes the
 * header actions off the row. Tightening the tracking buys about 120px, which
 * is the whole of the shortfall.
 */
@media (min-width: 961px) and (max-width: 1279px) {
    .nav {
        gap: 1.4rem;
    }

    .nav a {
        font-size: 0.72rem;
        letter-spacing: 0.08em;
    }
}

/* ---------------------------------------------------------------- header */
.site-header {
    position: sticky;
    top: 0;
    z-index: 60;
    /* --hero-scrim is the page ground as raw channels, so this follows the
       theme. A fixed dark value here painted a black bar across a light page
       and took the wordmark — which is var(--ink) — down with it. */
    background: rgba(var(--hero-scrim), 0.86);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-bottom: 1px solid transparent;
    transition: border-color var(--quick) var(--ease), background var(--quick) var(--ease);
}

.site-header.is-stuck {
    border-bottom-color: var(--rule);
}

.site-header__inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    min-width: 0;
    min-height: var(--header-h);
}

.wordmark {
    font-family: var(--font-display);
    font-size: 1.4rem;
    letter-spacing: 0.34em;
    text-transform: uppercase;
    text-indent: 0.34em;
    /* balances the trailing letter-space */
    white-space: nowrap;
    /* The wordmark yields before the icons do: a long site name truncates
       rather than pushing the actions off the side of a phone. */
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

.nav {
    display: flex;
    align-items: center;
    gap: 2.25rem;
}

.nav a {
    font-size: 0.78rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-soft);
    position: relative;
    padding-block: 0.35rem;
    transition: color var(--quick) var(--ease);
}

.nav a::after {
    content: '';
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 1px;
    background: var(--ink);
    transform: scaleX(0);
    transform-origin: right;
    transition: transform var(--slow) var(--ease);
}

.nav a:hover,
.nav a[aria-current='page'] {
    color: var(--ink);
}

.nav a:hover::after,
.nav a[aria-current='page']::after {
    transform: scaleX(1);
    transform-origin: left;
}

.header-actions {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    flex: 0 0 auto;
}

/* A hairline between the links and the actions. The two halves of the header
   do different jobs — one moves you, one acts on what you have — and without
   it the icon cluster reads as four more nav items set in pictures. Only
   drawn while the nav is actually there to be divided from. */
@media (min-width: 961px) {
    .header-actions::before {
        content: '';
        align-self: center;
        width: 1px;
        height: 20px;
        margin-right: 0.35rem;
        background: var(--rule);
    }
}

.icon-btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border: 0;
    background: transparent;
    color: var(--ink);
    cursor: pointer;
    border-radius: 50%;
    transition: background var(--quick) var(--ease);
}

.icon-btn:hover {
    background: var(--paper-sunken);
}

/* These had no keyboard state at all. */
.icon-btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The links beside these sit at --ink-soft and only come up to full ink on
   hover. The header icons were painted --ink outright, which left the
   right-hand end of the header brighter than the navigation it belongs to.
   They now follow the same rule the links do — and only here, because the
   same button closes the menu and the search overlay, where a control that
   dims itself is the last thing a modal needs. */
.header-actions .icon-btn {
    color: var(--ink-soft);
}

.header-actions .icon-btn:hover,
.header-actions .icon-btn:focus-visible {
    color: var(--ink);
}

.badge-count {
    position: absolute;
    top: 3px;
    right: 1px;
    min-width: 17px;
    height: 17px;
    padding: 0 4px;
    border-radius: 9px;
    background: var(--accent-deep);
    color: var(--paper);
    font-size: 0.625rem;
    font-weight: 600;
    line-height: 17px;
    text-align: center;
}

.badge-count[hidden] {
    display: none;
}

/* The account button, once there is a name behind it. Same monogram the phone
   home screen uses — a ring of the brand gold rather than a filled disc, so it
   reads as identity next to the flat icon row without outweighing it. */
.icon-btn__monogram {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border: 1px solid var(--accent);
    border-radius: 50%;
    background: var(--accent-wash);
    color: var(--accent-deep);
    font-family: var(--font-display);
    font-size: 0.95rem;
    font-weight: 600;
    line-height: 1;
}

/* ------------------------------------------------------- theme toggle */
/* One button, two glyphs, and CSS picks which is on screen from the same
   attribute that picks the palette — so the icon can never disagree with the
   page it is sitting on, including on the frame before app.js has run. It
   shows the theme you are going TO: a sun on the black page, a moon on the
   white one. */
.theme-icon--moon {
    display: none;
}

:root[data-theme="light"] .theme-icon--sun {
    display: none;
}

:root[data-theme="light"] .theme-icon--moon {
    display: block;
}

/* The glyph swap is instant; this gives the press somewhere to land. */
.icon-btn--theme svg {
    transition: transform var(--quick) var(--ease);
}

.icon-btn--theme:hover svg {
    transform: rotate(15deg);
}

/* Nothing switches without JS, and a control that does nothing is worse than
   no control — the artist's default still renders correctly either way. */
.no-js .icon-btn--theme,
.no-js .mobile-nav__theme {
    display: none;
}

.nav-toggle {
    display: none;
}

/* ------------------------------------------------------------ mobile nav */
.mobile-nav {
    position: fixed;
    inset: 0;
    z-index: 90;
    background: var(--paper);
    padding: 1.5rem var(--gutter) 3rem;
    transform: translateY(-100%);
    visibility: hidden;
    transition: transform var(--slow) var(--ease), visibility var(--slow);
    overflow-y: auto;
}

.mobile-nav.is-open {
    transform: translateY(0);
    visibility: visible;
}

.mobile-nav__head {
    display: flex;
    justify-content: space-between;
    align-items: center;
    min-height: 76px;
}

.mobile-nav__links {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin-top: 2rem;
}

.mobile-nav__links a {
    font-family: var(--font-display);
    font-size: 2rem;
    padding-block: 0.5rem;
    border-bottom: 1px solid var(--rule);
}

/* The one control in a column of destinations. It takes the links' type and
   rule so the column stays a column, and earns a right-hand value because it
   is the only row here whose state you cannot infer from where you are. */
.mobile-nav__theme {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    width: 100%;
    padding: 0.5rem 0;
    border: 0;
    border-bottom: 1px solid var(--rule);
    background: transparent;
    color: inherit;
    font-family: var(--font-display);
    font-size: 2rem;
    line-height: inherit;
    text-align: left;
    cursor: pointer;
}

.mobile-nav__theme-value {
    font-family: var(--font-body);
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

@media (max-width: 960px) {
    .nav {
        display: none;
    }

    .nav-toggle {
        display: inline-flex;
    }
}

/* ================================================================== hero
   The home page opens on one work instead of a collage of six. The painting is
   the ground; the pitch sits on a scrim drawn heavy at the reading edge and
   thin on the other side, so the type holds and the picture stays a picture.
   The work is credited by name underneath — a backdrop that is somebody's
   actual painting should say whose, and the credit doubles as the way in.

   What this replaces was a six-thumbnail grid sitting directly above the
   Featured section, which shows those same works larger and with prices.
   =========================================================================== */
.hero-art {
    position: relative;
    isolation: isolate;
    display: grid;
    align-content: center;
    min-height: min(74vh, 700px);
    padding-block: clamp(3rem, 7vw, 5.5rem);
    overflow: hidden;
    background: var(--paper);
}

/* The background proper: the same painting, blurred past legibility and held
   back, so the hero is lit by the work rather than papered with it. A portrait
   canvas in a wide band only ever shows one horizontal slice of itself, and
   the slice the scrim leaves visible is never the half worth seeing — so the
   picture is shown whole alongside instead, and only its colour is spread.
   Overscanned because a heavy blur eats its own edges. */
.hero-art__ground {
    position: absolute;
    inset: -10%;
    z-index: -2;
    background-image: var(--hero-art, none);
    background-position: 50% 45%;
    background-size: cover;
    filter: blur(70px) saturate(1.35);
    opacity: 0.72;
}

/* The scrim rides on the section, not on the ground — a filter applies to an
   element's own pseudo-elements too, and a blurred gradient is just fog. */
.hero-art::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: -1;
    background:
        linear-gradient(90deg,
            rgba(var(--hero-scrim), 0.88) 0%,
            rgba(var(--hero-scrim), 0.60) 48%,
            rgba(var(--hero-scrim), 0.44) 100%),
        linear-gradient(0deg,
            rgba(var(--hero-scrim), 1) 0%,
            rgba(var(--hero-scrim), 0) 42%);
}

.hero-art__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) clamp(240px, 27vw, 380px);
    align-items: center;
    gap: clamp(1.5rem, 4vw, 3rem);
    width: 100%;
    max-width: var(--shell);
    margin-inline: auto;
    padding-inline: var(--gutter);
}

.hero-art__content {
    max-width: 44rem;
}

.hero-art__eyebrow {
    margin: 0 0 1.15rem;
    font-size: 0.72rem;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: var(--accent);
}

.hero-art__title {
    margin: 0 0 1.1rem;
    /* Capped so "Discover Extraordinary" holds one line inside the measure
       above rather than breaking after "Discover". */
    font-size: clamp(2.3rem, 4.4vw, 3.9rem);
    line-height: 1.03;
    letter-spacing: -0.015em;
}

/* Carries the emphasis the old markup put on a highlight span. */
.hero-art__title em {
    display: block;
    font-style: italic;
    color: var(--accent-deep);
}

.hero-art__lead {
    margin: 0 0 2rem;
    max-width: 32rem;
    color: var(--ink-soft);
    line-height: 1.7;
}

.hero-art__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.8rem;
}

/* The work itself, shown whole. This is the hero's LCP, so it is a real
   <img> with dimensions on it rather than another background layer. */
.hero-art__plate {
    display: grid;
    gap: 0.9rem;
    justify-items: end;
}

.hero-art__plate img {
    display: block;
    width: 100%;
    height: auto;
    border-radius: var(--radius-art);
    box-shadow: var(--shadow-frame);
    transition: transform var(--slow) var(--ease);
}

.hero-art__plate:hover img {
    transform: translateY(-4px);
}

/* The gallery label for the work the whole screen is lit by. */
.hero-art__credit {
    display: grid;
    gap: 0.15rem;
    text-align: right;
}

.hero-art__credit-label {
    font-size: 0.62rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.hero-art__credit-title {
    font-family: var(--font-display);
    font-size: 1.15rem;
    line-height: 1.2;
    color: var(--ink);
    transition: color var(--quick) var(--ease);
}

.hero-art__plate:hover .hero-art__credit-title {
    color: var(--accent-deep);
}

.hero-art__credit-meta {
    font-size: 0.75rem;
    color: var(--ink-soft);
}

/* Past this the two columns stop fitting side by side, and the credit drops
   under the pitch — still a caption, not a button. */
/* Past this the painting stops earning its column and steps aside, leaving
   the wash of it behind the pitch. */
@media (max-width: 1023px) {
    .hero-art__inner {
        grid-template-columns: minmax(0, 1fr);
    }

    .hero-art__plate {
        display: none;
    }
}


/* ============================================= Mobile Gallery Hero
   Mobile-only gallery browsing interface. Hidden on desktop via display: none. */

.hero-mobile--mobile-only {
    display: none;
}

.hero-art--desktop-only {
    display: block;
}

@media (max-width: 700px) {
    .hero-art--desktop-only {
        display: none;
    }

    .hero-mobile--mobile-only {
        display: block;
    }

    /* The site header is the desktop's. Here the section's own top row —
       menu, cart, account — is the top bar, exactly as the design draws it. */
    .page-home .site-header {
        display: none;
    }

    /* The splash is all this section holds now, and it runs edge to edge —
       so the gutter belongs to the splash's own content, not to the section
       that then has to be broken back out of. */
    /*
     * One screen, exactly. The section is a column that claims the whole
     * viewport and lets the mosaic take whatever the ticker and the words
     * leave — so the hero fills a tall phone instead of stopping short, and
     * still fits a short one instead of pushing the button under the fold.
     *
     * dvh, not vh: on a phone browser 100vh is the height *without* the
     * address bar, so a vh-sized hero is always taller than the glass and
     * the last line is cut off until the toolbar collapses. min-height
     * rather than height, so an unusually long headline can still grow the
     * section rather than being clipped by it.
     */
    .hero-mobile {
        position: relative;
        isolation: isolate;
        display: flex;
        flex-direction: column;
        min-height: 100vh;
        min-height: 100dvh;
    }

    /* The same painting behind the greeting — blurred, as it is on the wide
       hero, and for the same reason. Held sharp it was detail competing with
       every control laid over it; blurred it becomes the colour the screen is
       lit by, and that is what lets the veil below come down from 0.80 to 0.42
       so the picture reads as warmth rather than murk.
       Overscanned on three sides because a heavy blur eats its own edges. */
    /* The splash. One work, edge to edge, the height of the screen: the bar
       rides its head and the gallery plate sits at its foot. svh rather than
       vh so a phone's collapsing toolbars cannot crop the plate off. */
    /* ---------------------------------------------------------- mosaic */
    /*
     * Three columns, two tiles each. The middle column splits lower than the
     * outer two, and that stagger is the whole effect — six tiles on one line
     * read as a product listing rather than a wall of work.
     *
     * The band is a fraction of the viewport rather than a fixed height, so
     * the headline underneath still clears the fold on a short phone.
     */
    /* --------------------------------------------------------- the wall */
    /*
     * A solid rectangle of paintings, none of them cropped, drifting right to
     * left.
     *
     * The columns are justified: PHP works out each column's width as a
     * fraction of the band (--cw) such that the heights of the pieces in it
     * add up to exactly the band. So every column reaches top and bottom with
     * no remainder, the block has straight edges, and each painting keeps its
     * own proportions — no object-fit crop anywhere.
     */
    .home-m__mosaic {
        --gap: 6px;
        --band: max(150px, min(330px, calc(100dvh - 410px)));
        overflow: hidden;
    }

    .home-m__wall {
        display: flex;
        align-items: flex-start;
        gap: var(--gap);
        height: var(--band);
        width: max-content;
        animation: home-m-drift 96s linear infinite;
    }

    .home-m__col {
        display: flex;
        flex-direction: column;
        flex: none;
        gap: var(--gap);
        /*
         * The pieces have to share the band with the gaps stacked between
         * them, so what is left for the paintings is the band minus (n - 1)
         * gaps — and the column narrows to match, or the heights would
         * overrun and the block would lose its straight bottom edge.
         */
        /*
         * The fallbacks matter. A var() with no value makes the whole calc()
         * invalid, the width falls back to auto, and a column of
         * width:100% children then collapses — which is what a stylesheet
         * uploaded ahead of its index.php did to the live wall. With --n
         * defaulting to 1 the sum degrades to the old no-gap width instead
         * of to nothing.
         */
        width: calc((var(--band) - (var(--n, 1) - 1) * var(--gap)) * var(--cw));
        height: 100%;
    }

    /*
     * One copy exactly — see the note in index.php for where the three terms
     * come from. Not -50%, which is one gap out and would nudge the wall
     * every time it came round.
     */
    @keyframes home-m-drift {
        from { transform: translateX(0); }
        to {
            /* Same reasoning: with no gap terms this is the old distance,
               which is very slightly short rather than broken. */
            transform: translateX(calc(
                -1 * (var(--band) * var(--span)
                      - var(--gap) * var(--gapspan, 0)
                      + var(--gap) * var(--cols, 0))
            ));
        }
    }

    @media (prefers-reduced-motion: reduce) {
        .home-m__wall {
            animation: none;
        }
    }

    .home-m__tile {
        display: block;
        width: 100%;
        /* The painting's own shape. Its height therefore falls out of the
           column width, and the column width was chosen so these sum to the
           band — which is what makes the rectangle solid. */
        aspect-ratio: var(--ar);
        overflow: hidden;
        /* Sharp corners, as a canvas has. */
        border-radius: 0;
        background: var(--paper-sunken);
    }

    .home-m__tile img {
        display: block;
        width: 100%;
        height: 100%;
        /*
         * contain, not cover. The box is built from this file's own
         * proportions, so there is nothing to crop and the two agree — but
         * if a thumbnail is ever regenerated at a different ratio, contain
         * shows the whole painting slightly letterboxed rather than
         * silently cutting a piece off it.
         */
        object-fit: contain;
    }

    /* ---------------------------------------------------------- ticker */
    .home-m__ticker {
        flex: none;
        overflow: hidden;
        margin-block: 1.5rem 1.9rem;
        padding-block: 0.2rem;
        /* Fades the words out at both edges instead of cutting them. */
        -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
        mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
    }

    .home-m__ticker-track {
        display: flex;
        align-items: center;
        gap: 0.9rem;
        width: max-content;
        /* The track holds two copies of the list, so travelling exactly half
           its width lands on the identical frame and the loop is seamless. */
        animation: home-m-ticker 26s linear infinite;
    }

    .home-m__ticker-word,
    .home-m__ticker-dot {
        font-size: 0.6875rem;
        font-weight: 500;
        letter-spacing: 0.22em;
        text-transform: uppercase;
        white-space: nowrap;
        color: var(--ink-faint);
    }

    .home-m__ticker-dot {
        color: var(--accent);
        letter-spacing: 0;
    }

    @keyframes home-m-ticker {
        from { transform: translateX(0); }
        to   { transform: translateX(-50%); }
    }

    /* Anything that moves on its own stops for this preference. */
    @media (prefers-reduced-motion: reduce) {
        .home-m__ticker-track {
            animation: none;
        }
    }

    /* ----------------------------------------------------------- intro */
    .home-m__intro {
        flex: none;
        /* Takes whatever the marquee and the ticker leave, which is what
           keeps the hero exactly one screen tall now that the rows are
           sized by their own content. */
        margin-top: auto;
        padding: 0 var(--gutter) calc(2.25rem + env(safe-area-inset-bottom));
    }

    .home-m__intro-title {
        margin: 0 0 0.9rem;
        font-size: clamp(2.6rem, 12vw, 3.6rem);
        line-height: 1.02;
        letter-spacing: -0.015em;
    }

    .home-m__intro-lead {
        margin: 0 0 1.75rem;
        max-width: 30ch;
        font-size: 0.9375rem;
        line-height: 1.6;
        color: var(--ink-soft);
    }

    /* Sits at the right, as the design has it — the eye finishes the
       sentence and lands on it. */
    .home-m__intro-cta {
        display: inline-flex;
        align-items: center;
        gap: 0.6rem;
        float: right;
        padding: 0.85rem 1.5rem;
        border: 1px solid var(--rule-strong);
        border-radius: 999px;
        font-family: var(--font-display);
        font-size: 1.05rem;
        color: var(--ink);
        transition: border-color var(--quick) var(--ease), background var(--quick) var(--ease);
    }

    .home-m__intro-cta:active {
        background: var(--paper-raised);
    }

    .home-m__intro-cta svg {
        width: 18px;
        height: 18px;
    }

    /* The float needs closing, or the section below rides up beside it. */
    .home-m__intro::after {
        content: '';
        display: block;
        clear: both;
    }

    /* ------------------------------------------- the rest of the page */
    /*
     * On a phone the home screen is the hero and nothing else: mosaic,
     * ticker, headline, one way in. Everything below it — the artist panel,
     * the latest work, prints, mockups, the video, viewings, testimonials
     * and the category list — belongs to the desktop page.
     *
     * Hidden, not deleted: these nine sections are the desktop homepage, and
     * removing the markup would take them off both. The footer sits outside
     * <main>, so it is untouched and still carries the links.
     */
    body.page-home main > section:not(.hero-mobile) {
        display: none;
    }

    /*
     * And the footer with them. Scoped to the home screen: every other page
     * on the phone still needs it, and on the shop and artwork pages it is
     * the only place some of those links appear at all.
     */
    body.page-home .site-footer {
        display: none;
    }









    /* ----------------------------------------------------------- profile */

}


/* ------------------------------------------------------------ artwork grid */
.art-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
    gap: clamp(1.5rem, 3vw, 2.75rem) clamp(1.25rem, 2.5vw, 2rem);
}

.art-grid--two {
    grid-template-columns: repeat(auto-fill, minmax(min(420px, 100%), 1fr));
}

.art-grid--dense {
    grid-template-columns: repeat(auto-fill, minmax(min(230px, 100%), 1fr));
}

/* Masonry, via CSS columns. Degrades to the grid above where unsupported,
   and unlike a JS masonry it does not reflow after every image loads. */
.art-masonry {
    columns: 4 280px;
    column-gap: clamp(1.25rem, 2.5vw, 2rem);
}

.art-masonry>* {
    break-inside: avoid;
    margin-bottom: clamp(1.5rem, 3vw, 2.75rem);
}

@media (max-width: 600px) {
    .art-grid {
        grid-template-columns: repeat(2, 1fr);
        gap: 1rem;
    }

    .art-grid--two {
        grid-template-columns: repeat(2, 1fr);
    }

    .art-grid--dense {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 700px) {
    .art-masonry {
        columns: 2 150px;
    }
}

/* Column counts step with the viewport so a column never falls below the
   width at which a card's caption starts wrapping to three lines. */
@media (min-width: 901px) and (max-width: 1200px) {
    .art-masonry {
        columns: 3 240px;
        column-gap: clamp(1rem, 2vw, 1.5rem);
    }
}

@media (min-width: 1201px) and (max-width: 1600px) {
    .art-masonry {
        columns: 4 280px;
        column-gap: clamp(1.25rem, 2.5vw, 2rem);
    }
}

@media (min-width: 1601px) {
    .art-masonry {
        columns: 5 280px;
        column-gap: clamp(1.5rem, 3vw, 2.5rem);
    }
}

/* ---------------------------------------------------------- artwork card */
.art-card {
    position: relative;
}

/* Rounded, because on a dark ground a hard-cornered rectangle of artwork
   reads as a hole cut in the page rather than a piece hung on it. */
.art-card__frame {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: var(--radius-art);
    background: var(--paper-sunken);
}

/* Fixed-ratio variant for grids that must line up; masonry omits this so
   each piece keeps its own proportions. */
.art-card__frame--ratio {
    aspect-ratio: 4 / 5;
}

.art-card__frame img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform var(--slow) var(--ease), filter var(--slow) var(--ease);
}

.art-masonry .art-card__frame img {
    height: auto;
    object-fit: contain;
}

.art-card:hover .art-card__frame img {
    transform: scale(1.035);
}

.art-card__overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 1.1rem;
    background: linear-gradient(to top, rgba(23, 22, 19, 0.42), rgba(23, 22, 19, 0) 55%);
    opacity: 0;
    transition: opacity var(--quick) var(--ease);
}

.art-card:hover .art-card__overlay,
.art-card:focus-within .art-card__overlay {
    opacity: 1;
}

.art-card__quick {
    background: var(--paper);
    color: var(--ink);
    border: 0;
    padding: 0.7rem 1.3rem;
    font-size: 0.7rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    cursor: pointer;
    transform: translateY(6px);
    transition: transform var(--quick) var(--ease), background var(--quick) var(--ease);
}

.art-card:hover .art-card__quick {
    transform: translateY(0);
}

.art-card__quick:hover {
    background: var(--accent-wash);
}

.art-card__body {
    padding-top: 1rem;
}

.art-card__title {
    font-family: var(--font-display);
    font-size: 1.2rem;
    line-height: 1.25;
    margin: 0 0 0.2rem;
}

.art-card__meta {
    font-size: 0.8125rem;
    color: var(--ink-faint);
    margin: 0 0 0.5rem;
}

.art-card__price {
    font-size: 0.9375rem;
    color: var(--ink);
    letter-spacing: 0.01em;
}

.art-card__price .from {
    color: var(--ink-faint);
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* Save (wishlist) control. */
.save-btn {
    position: absolute;
    top: 0.85rem;
    right: 0.85rem;
    width: 38px;
    height: 38px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 50%;
    /* Stays dark in both themes: it floats over the thumbnail, not the page.
       The mark on it therefore cannot be var(--ink), which follows the page. */
    background: rgba(23, 22, 19, 0.82);
    color: var(--ink-surface-strong);
    cursor: pointer;
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity var(--quick) var(--ease), transform var(--quick) var(--ease),
        background var(--quick) var(--ease), color var(--quick) var(--ease);
}

.art-card:hover .save-btn,
.save-btn.is-saved,
.save-btn:focus-visible {
    opacity: 1;
    transform: translateY(0);
}

.save-btn.is-saved {
    background: var(--ink);
    color: var(--paper);
}

.save-btn:hover {
    background: var(--ink);
    color: var(--paper);
}

/* Status ribbon: SOLD, and only where it is true. */
/* Inverting the palette turned this into a bright slab across the corner of
   every piece. On a dark ground the label only has to be legible, not loud,
   so it goes back to dark-on-light-text with a blur behind it. */
.art-status {
    position: absolute;
    top: 0.85rem;
    left: 0.85rem;
    padding: 0.35rem 0.8rem;
    border-radius: 999px;
    /* A pill over the artwork. Dark ground in both themes, so its text uses
       the ink-surface tokens, which stay near-white in both. */
    background: rgba(10, 10, 12, 0.72);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    color: var(--ink-surface-strong);
    font-size: 0.625rem;
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
}

/* On a two-up phone grid the card is ~160px wide, so a long status such as
   "Original — 1 only" would wrap underneath the save button. Cap it. */
.art-status {
    max-width: calc(100% - 3.6rem);
}

@media (max-width: 700px) {
    .art-status {
        top: 0.5rem;
        left: 0.5rem;
        padding: 0.25rem 0.5rem;
        font-size: 0.5625rem;
        letter-spacing: 0.12em;
    }

    .save-btn {
        top: 0.5rem;
        right: 0.5rem;
        width: 34px;
        height: 34px;
    }
}

.art-status--sold {
    background: rgba(10, 10, 12, 0.82);
    color: var(--ink-surface-text);
}

.art-status--featured {
    background: rgba(201, 164, 106, 0.22);
    color: var(--ink-surface-strong);
}

.art-status--last {
    background: rgba(224, 112, 95, 0.20);
    color: var(--ink-surface-strong);
}

.art-status--digital {
    background: rgba(126, 156, 184, 0.28);
    color: var(--ink-surface-strong);
}

@media (max-width: 480px) {

    /* Two cards across ~350px: the default 1.2rem title and 0.94rem price
       wrap to three lines each. A notch smaller keeps each card to one idea. */
    .art-card__body {
        padding-top: 0.75rem;
    }

    .art-card__title {
        font-size: 1.05rem;
    }

    .art-card__meta {
        font-size: 0.75rem;
        margin-bottom: 0.35rem;
    }

    .art-card__price {
        font-size: 0.875rem;
    }

    .art-card__price .from {
        font-size: 0.6875rem;
        letter-spacing: 0.06em;
    }
}

/* -------------------------------------------------------- artwork detail */
.artwork-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.35fr) minmax(340px, 0.85fr);
    gap: clamp(2rem, 5vw, 4.5rem);
    align-items: start;
}

@media (max-width: 1000px) {
    /* minmax(0, 1fr), not 1fr: a bare 1fr leaves the column's minimum at
       auto, so a grid item is free to size to its own min-content and push
       out past the column — which is how the viewer and the buy panel ended
       up 594px wide inside a 350px phone. */
    .artwork-layout {
        grid-template-columns: minmax(0, 1fr);
    }
}

.viewer {
    position: sticky;
    top: calc(var(--header-h) + 1.5rem);
}

@media (max-width: 1000px) {
    .viewer {
        position: static;
    }
}

.viewer__stage {
    /*
       One frame for every kind of media.

       This used to be `width: fit-content`, shrink-wrapped to the picture, so
       the box was a different size on every slide and the video block below it
       ran the full column — a narrow plate above a wide one. A gallery that
       mixes a painting with a 16:9 clip has to settle on a frame first and fit
       the media into it, which is the one thing fit-content cannot do.

       The ratio is the lead image's own (set inline as --stage-ar), so the
       picture the page is about still fills the frame edge to edge. Everything
       else is contained inside it and centred.
    */
    --stage-ar: 0.8;
    /* The 11rem is not padding — it is what sits ABOVE the picture before it
       even starts: the breadcrumb, the section's top padding and the gap under
       the header, measured at 239px on a 1500x950 window. svh, not vh: on a
       phone vh is the height with the browser chrome retracted, so the cap
       would still be taller than what is actually on screen when you arrive. */
    --stage-cap: max(20rem, calc(100svh - var(--header-h) - 11rem));
    position: relative;
    /*
       Capped by WIDTH, not by height, even though the limit being expressed is
       a height. `max-height` on a ratio box does not narrow it: the box stays
       the column's full width, keeps the capped height and the ratio quietly
       breaks, which put a portrait painting in a nearly square frame with a
       gutter down each side. Turning the cap into a width — the height it is
       allowed, times the ratio — leaves the ratio exact at every size, so the
       lead image fills its frame and only the other media are contained.
    */
    width: 100%;
    max-width: calc(var(--stage-cap) * var(--stage-ar));
    aspect-ratio: var(--stage-ar);
    margin-inline: auto;
    background: var(--paper-sunken);
    border: 1px solid var(--rule-strong);
    border-radius: 18px;
    overflow: hidden;
    cursor: zoom-in;
}

/* With no JavaScript the track is a plain vertical list of every image, which
   a fixed-height frame would crop to the first one. */
.no-js .viewer__stage {
    aspect-ratio: auto;
    max-width: none;
    cursor: default;
}

/* A clip carries its own fullscreen button and the lightbox only holds an
   <img>, so neither the zoom nor the expand control belongs on a video. */
.viewer__stage.is-video {
    cursor: default;
}

.viewer__stage.is-video .viewer__tools {
    display: none;
}

.viewer__stage img,
.viewer__stage video,
.viewer__stage iframe {
    display: block;
    width: 100%;
    height: 100%;
    /* contain, not cover: cropping a painting to fill a frame is the one thing
       a gallery may not do. */
    object-fit: contain;
    transition: transform var(--slow) var(--ease);
}

.viewer__stage iframe {
    border: 0;
}

.viewer__stage video {
    background: #000;
}

.viewer__stage.is-zoomed {
    cursor: zoom-out;
}

/* Scaling every slide is harmless: only one is on screen, and transform does
   not affect layout, so the snap widths are untouched. */
.viewer__stage.is-zoomed img {
    transform: scale(2);
}

/*
 * The slide track. With JavaScript off this is a plain vertical list — every
 * image visible, each a real <img> with real alt text — which is a perfectly
 * good way to look at a set of photographs. With JS it becomes a horizontal
 * scroll-snap strip, so swipe and momentum are the browser's own.
 */
.viewer__track {
    display: grid;
    gap: 0.6rem;
}

html:not(.no-js) .viewer__track {
    height: 100%;
}

html:not(.no-js) .viewer__track {
    display: flex;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;
    /* Horizontal panning belongs to the track; vertical stays with the page. */
    touch-action: pan-y pinch-zoom;
}

html:not(.no-js) .viewer__track::-webkit-scrollbar {
    display: none;
}

html:not(.no-js) .viewer__slide {
    flex: 0 0 100%;
    scroll-snap-align: center;
}

.viewer__slide {
    margin: 0;
    min-width: 0;
}

/* The media takes the frame; a caption, when there is one, sits under it and
   the picture gives up exactly that much height rather than overflowing. */
html:not(.no-js) .viewer__slide {
    display: flex;
    flex-direction: column;
    height: 100%;
}

html:not(.no-js) .viewer__slide > picture,
html:not(.no-js) .viewer__slide > video,
html:not(.no-js) .viewer__slide > iframe {
    flex: 1;
    min-height: 0;
}

html:not(.no-js) .viewer__slide > picture {
    display: block;
}

.viewer__caption {
    padding: 0.5rem 0.75rem;
    font-size: 0.78rem;
    color: var(--ink-faint);
}

@media (prefers-reduced-motion: reduce) {
    .viewer__track {
        scroll-behavior: auto;
    }
}

.viewer__nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    border: 0;
    border-radius: 50%;
    /* Same reasoning as .viewer__tool: it lands on the artwork, so the ground
       stays dark in both themes and the arrow takes an ink-surface token. */
    background: rgba(23, 22, 19, 0.85);
    color: var(--ink-surface-strong);
    cursor: pointer;
    z-index: 2;
    transition: opacity var(--quick) var(--ease);
}

.viewer__nav--prev {
    left: 1rem;
}

.viewer__nav--next {
    right: 1rem;
}

.viewer__nav[disabled] {
    opacity: 0.3;
    pointer-events: none;
}

.viewer__count {
    position: absolute;
    left: 1rem;
    bottom: 1rem;
    margin: 0;
    padding: 0.25rem 0.6rem;
    border-radius: 999px;
    background: rgba(10, 10, 12, 0.72);
    color: var(--ink-surface-strong);
    font-size: 0.7rem;
    letter-spacing: 0.08em;
    z-index: 2;
}

/* Controls that only mean something once the script is driving the track. */
.no-js .viewer__nav,
.no-js .viewer__count {
    display: none;
}

.viewer__tools {
    position: absolute;
    right: 1rem;
    bottom: 1rem;
    display: flex;
    gap: 0.5rem;
}

.viewer__tool {
    width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    /* Sits on the artwork stage, so the ground stays dark in both themes and
       the icon takes the ink-surface token rather than the page's ink. */
    background: rgba(23, 22, 19, 0.85);
    border: 0;
    color: var(--ink-surface-strong);
    cursor: pointer;
    transition: background var(--quick) var(--ease), color var(--quick) var(--ease);
}

.viewer__tool:hover {
    /* Hover flips the ground to the page's own paper, so the icon has to flip
       with it — var(--ink) is the right colour on var(--paper) in either theme. */
    background: var(--paper);
    color: var(--ink);
}

.viewer__thumbs {
    display: flex;
    gap: 0.6rem;
    margin-top: 0.75rem;
    overflow-x: auto;
    padding-bottom: 0.35rem;
    scrollbar-width: thin;
}

.viewer__thumb {
    flex: 0 0 84px;
    aspect-ratio: 1;
    border: 1px solid transparent;
    background: var(--paper-sunken);
    cursor: pointer;
    padding: 0;
    overflow: hidden;
}

.viewer__thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.viewer__thumb.is-active {
    border-color: var(--ink);
}

/* A video in the rail has to be readable as a video before it is clicked,
   otherwise the arrows run past a slide nobody expected. */
.viewer__thumb--video {
    position: relative;
}

.viewer__thumb-play {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    color: #fff;
    background: rgba(0, 0, 0, 0.35);
    pointer-events: none;
}

.viewer__thumb-play svg {
    padding: 6px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.6);
}

/* A clip with no poster has nothing behind the badge, so the scrim that makes
   it legible over a photograph only muddies the empty tile. The glyph carries
   itself instead. */
.viewer__thumb--bare .viewer__thumb-play {
    background: none;
    color: var(--ink-faint);
}

.viewer__thumb--bare .viewer__thumb-play svg {
    width: 20px;
    height: 20px;
    padding: 0;
    background: none;
    box-shadow: none;
}

/* Fullscreen lightbox. */
.lightbox {
    /* The frame the picture is measured against. It has to be one value the
       image can subtract, because the image is sized off the viewport and the
       padding is not part of that: at 2rem with a 96vw image the picture came
       out 64px wider than the box holding it, so centring had nothing left to
       centre and it hung off the right edge of a phone. */
    --lightbox-pad: clamp(0.75rem, 4vw, 2rem);

    position: fixed;
    inset: 0;
    z-index: 200;
    background: rgba(18, 17, 15, 0.97);
    display: none;
    place-items: center;
    padding: var(--lightbox-pad);
}

.lightbox.is-open {
    display: grid;
}

.lightbox img {
    /* Stated against the viewport minus the padding rather than as a
       percentage: a percentage max-height resolves against the grid row, the
       row is sized by this image, and the browser resolves that circle by
       ignoring the cap. dvh, so a phone's collapsing toolbar cannot crop it. */
    max-width: calc(100vw - var(--lightbox-pad) * 2);
    max-height: calc(100dvh - var(--lightbox-pad) * 2);
    width: auto;
    height: auto;
    object-fit: contain;
}

.lightbox__close {
    position: absolute;
    /* The <img> is the later sibling, so without this the close button sits
       under a picture that now fills nearly the whole screen. */
    z-index: 1;
    top: calc(1.5rem + env(safe-area-inset-top));
    right: calc(1.5rem + env(safe-area-inset-right));
    width: 46px;
    height: 46px;
    display: grid;
    place-items: center;
    /* Its own ground: centred on the artwork it can land on any colour, and a
       transparent button over a painting is not reliably findable. */
    background: rgba(18, 17, 15, 0.72);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    border-radius: 50%;
    border: 1px solid rgba(242, 240, 236, 0.28);
    /* An ink-surface token, not --ink. The lightbox scrim is near-black in both
       themes — it darkens the room around a painting — but --ink follows the
       page, so on a light page the × turned black on black and the button read
       as an empty box. --ink-surface-strong stays near-white in both. */
    color: var(--ink-surface-strong);
    font-size: 1.25rem;
    cursor: pointer;
}

/* ------------------------------------------------------- purchase panel */
.buy-panel {}

.buy-panel__title {
    font-size: clamp(2rem, 3.6vw, 2.85rem);
    margin-bottom: 0.35rem;
}

.buy-panel__medium {
    color: var(--ink-faint);
    font-size: 0.95rem;
    margin-bottom: 1.5rem;
}

.spec-list {
    list-style: none;
    margin: 0 0 2rem;
    padding: 0;
}

.spec-list li {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 0.25rem 1.5rem;
    padding: 0.7rem 0;
    border-bottom: 1px solid var(--rule);
    font-size: 0.9rem;
}

.spec-list dt,
.spec-list .label {
    color: var(--ink-faint);
}

.spec-list .value {
    text-align: right;
}

.price-block {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
    margin-bottom: 1.5rem;
}

.price-block__amount {
    font-family: var(--font-display);
    font-size: 2.1rem;
    line-height: 1;
}

.price-block__compare {
    color: var(--ink-faint);
    text-decoration: line-through;
    font-size: 1rem;
}

/* Buying options: original vs print. */
.option-group {
    margin-bottom: 1.75rem;
}

.option-group__label {
    font-size: 0.6875rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin-bottom: 0.75rem;
    display: block;
}

.option-tabs {
    display: flex;
    gap: 0;
    border: 1px solid var(--rule-strong);
}

.option-tab {
    flex: 1;
    padding: 0.85rem 1rem;
    background: transparent;
    border: 0;
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-soft);
    cursor: pointer;
    transition: background var(--quick) var(--ease), color var(--quick) var(--ease);
}

.option-tab.is-active {
    background: var(--ink);
    color: var(--paper);
}

.option-tab[disabled] {
    color: var(--ink-faint);
    cursor: not-allowed;
    text-decoration: line-through;
}

/* Size chooser — always shows real centimetres next to the paper name. */
.size-options {
    display: grid;
    gap: 0.5rem;
}

.size-option {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.85rem 1.1rem;
    border: 1px solid var(--rule-strong);
    background: var(--paper-raised);
    cursor: pointer;
    text-align: left;
    transition: border-color var(--quick) var(--ease), background var(--quick) var(--ease);
}

.size-option:hover {
    border-color: var(--ink-faint);
}

.size-option.is-active {
    border-color: var(--ink);
    background: var(--accent-wash);
}

.size-option__name {
    font-weight: 500;
    font-size: 0.9375rem;
}

.size-option__dims {
    font-size: 0.8125rem;
    color: var(--ink-faint);
}

.size-option__price {
    font-size: 0.9375rem;
    white-space: nowrap;
}

.qty-row {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1.25rem;
}

.qty {
    display: inline-flex;
    align-items: center;
    border: 1px solid var(--rule-strong);
}

.qty button {
    width: 42px;
    height: 46px;
    background: transparent;
    border: 0;
    font-size: 1.1rem;
    color: var(--ink);
    cursor: pointer;
}

.qty button:hover:not([disabled]) {
    background: var(--paper-sunken);
}

.qty input {
    width: 52px;
    height: 46px;
    border: 0;
    border-inline: 1px solid var(--rule-strong);
    text-align: center;
    font-family: var(--font-body);
    font-size: 0.95rem;
    background: transparent;
    -moz-appearance: textfield;
}

.qty input::-webkit-outer-spin-button,
.qty input::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}

.sold-notice {
    padding: 1.15rem 1.35rem;
    background: var(--paper-sunken);
    border-left: 2px solid var(--ink);
    margin-bottom: 1.5rem;
}

.sold-notice strong {
    letter-spacing: 0.18em;
    text-transform: uppercase;
    font-size: 0.75rem;
}

/* -------------------------------------------------- size visualiser */
.visualiser {
    background: var(--paper-sunken);
    padding: clamp(1.25rem, 3vw, 2rem);
}

.visualiser__stage {
    position: relative;
    background: var(--paper-raised);
    border: 1px solid var(--rule);
    overflow: hidden;
    /* A room-ish aspect so the reference objects have somewhere to stand. */
    aspect-ratio: 16 / 10;
}

.visualiser__floor {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    height: 14%;
    background: var(--paper-sunken);
}

.visualiser__art {
    position: absolute;
    background: var(--ink);
    box-shadow: var(--shadow-frame);
    transition: width var(--slow) var(--ease), height var(--slow) var(--ease);
}

.visualiser__art img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.visualiser__ref {
    position: absolute;
    bottom: 14%;
    opacity: 0.5;
}

.visualiser__ref svg {
    display: block;
    height: 100%;
    width: auto;
}

.visualiser__scale {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: 1rem;
}

.visualiser__note {
    margin-top: 1rem;
    font-size: 0.8125rem;
    color: var(--ink-faint);
}

/* ------------------------------------------------------------- filters */
.filter-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
    padding-block: 1.1rem;
    border-block: 1px solid var(--rule);
    margin-bottom: clamp(2rem, 4vw, 3rem);
}

.filter-bar__count {
    font-size: 0.8125rem;
    color: var(--ink-faint);
}

.select {
    appearance: none;
    -webkit-appearance: none;
    background: transparent url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='none' stroke='%238b867c' stroke-width='1.3' d='M1 1l4 4 4-4'/%3E%3C/svg%3E") no-repeat right 0.35rem center;
    border: 0;
    border-bottom: 1px solid var(--rule-strong);
    padding: 0.45rem 1.6rem 0.45rem 0;
    font-family: var(--font-body);
    font-size: 0.8125rem;
    color: var(--ink);
    cursor: pointer;
}

/* Stickiness belongs to the panel that wraps this form (.collection__aside),
   which knows how tall the toolbar above it is. */
.facets {
    position: static;
}

.facet {
    border-bottom: 1px solid var(--rule);
    padding-block: 1.1rem;
}

.facet:first-child {
    padding-top: 0;
}

.facet__title {
    display: flex;
    justify-content: space-between;
    align-items: center;
    width: 100%;
    background: none;
    border: 0;
    padding: 0;
    font-family: var(--font-body);
    font-size: 0.6875rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink);
    cursor: pointer;
}

.facet__body {
    padding-top: 0.9rem;
    display: grid;
    gap: 0.5rem;
}

.facet__body[hidden] {
    display: none;
}

.facet-option {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.875rem;
    color: var(--ink-soft);
    cursor: pointer;
}

.facet-option input {
    accent-color: var(--accent-deep);
    width: 15px;
    height: 15px;
}

.facet-option:hover {
    color: var(--ink);
}

.facet-option .count {
    margin-left: auto;
    color: var(--ink-faint);
    font-size: 0.75rem;
}

.price-range {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.price-range input {
    width: 100%;
    border: 1px solid var(--rule-strong);
    background: var(--paper-raised);
    padding: 0.5rem 0.6rem;
    font-family: var(--font-body);
    font-size: 0.8125rem;
}

.active-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1.5rem;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.35rem 0.5rem 0.35rem 0.8rem;
    background: var(--paper-sunken);
    font-size: 0.75rem;
    color: var(--ink-soft);
}

.chip button {
    border: 0;
    background: none;
    cursor: pointer;
    color: var(--ink-faint);
    font-size: 0.9rem;
    line-height: 1;
    padding: 0 0.15rem;
}

.chip button:hover {
    color: var(--danger);
}

/* ------------------------------------------------------------- search */
.search-overlay {
    position: fixed;
    inset: 0;
    z-index: 100;
    /* Follows the theme: the search input inside is var(--ink). */
    background: rgba(var(--hero-scrim), 0.97);
    backdrop-filter: blur(8px);
    display: none;
    padding-top: 14vh;
}

.search-overlay.is-open {
    display: block;
}

.search-overlay input {
    width: 100%;
    border: 0;
    border-bottom: 1px solid var(--rule-strong);
    background: transparent;
    font-family: var(--font-display);
    font-size: clamp(1.75rem, 4vw, 3rem);
    padding: 0.5rem 0;
    color: var(--ink);
}

.search-overlay input:focus {
    outline: none;
    border-bottom-color: var(--ink);
}

.search-results {
    margin-top: 2rem;
    display: grid;
    gap: 0.25rem;
    max-height: 55vh;
    overflow-y: auto;
}

.search-result {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.7rem;
    transition: background var(--quick) var(--ease);
}

.search-result:hover,
.search-result.is-highlighted {
    background: var(--paper-sunken);
}

.search-result img {
    width: 56px;
    height: 56px;
    object-fit: cover;
    background: var(--paper-sunken);
}

.search-result__title {
    font-family: var(--font-display);
    font-size: 1.1rem;
}

.search-result__meta {
    font-size: 0.78rem;
    color: var(--ink-faint);
}

/* ------------------------------------------------------------ pagination */
.pagination {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 0.4rem;
    margin-top: clamp(2.5rem, 5vw, 4rem);
}

.pagination a,
.pagination span {
    min-width: 40px;
    height: 40px;
    display: grid;
    place-items: center;
    padding-inline: 0.6rem;
    font-size: 0.875rem;
    color: var(--ink-soft);
    border: 1px solid transparent;
    transition: border-color var(--quick) var(--ease), color var(--quick) var(--ease);
}

.pagination a:hover {
    border-color: var(--rule-strong);
    color: var(--ink);
}

.pagination .is-current {
    border-color: var(--ink);
    color: var(--ink);
}

.pagination .is-disabled {
    color: var(--rule-strong);
    pointer-events: none;
}

/* ---------------------------------------------------------------- forms */
.field {
    margin-bottom: 1.25rem;
}

.field label {
    display: block;
    font-size: 0.6875rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin-bottom: 0.5rem;
}

.field input[type="text"],
.field input[type="email"],
.field input[type="tel"],
.field input[type="password"],
.field input[type="number"],
.field input[type="date"],
.field input[type="search"],
.field select,
.field textarea {
    width: 100%;
    padding: 0.8rem 0.95rem;
    border: 1px solid var(--rule-strong);
    background: var(--paper-raised);
    font-family: var(--font-body);
    font-size: 0.9375rem;
    color: var(--ink);
    transition: border-color var(--quick) var(--ease);
}

.field textarea {
    min-height: 130px;
    resize: vertical;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
    outline: none;
    border-color: var(--ink);
}

.field .hint {
    font-size: 0.78rem;
    color: var(--ink-faint);
    margin-top: 0.4rem;
}

.field .error {
    font-size: 0.8125rem;
    color: var(--danger);
    margin-top: 0.4rem;
}

.field.has-error input,
.field.has-error select,
.field.has-error textarea {
    border-color: var(--danger);
}

.field-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
}

@media (max-width: 600px) {
    .field-row {
        grid-template-columns: 1fr;
    }
}

.check {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    font-size: 0.875rem;
    color: var(--ink-soft);
    cursor: pointer;
}

.check input {
    margin-top: 0.25rem;
    accent-color: var(--accent-deep);
}

/* ---------------------------------------------------------------- cart */
/*
   The page reads as two blocks side by side: the order, and what it comes to.
   Both are panels rather than loose content, so the pairing is legible before
   a word is read — and both stay put when the cart is empty, which is why the
   empty state lives inside the order panel instead of replacing the layout.
*/
.cart-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.6fr) minmax(300px, 0.8fr);
    gap: clamp(2rem, 4vw, 4rem);
    align-items: start;
}

@media (max-width: 900px) {

    /* minmax(0, …) rather than a bare 1fr. Nothing in the panel is wide enough
       to overflow today — this is the house guard against a auto minimum
       sizing the column to its content the moment something wider lands in it. */
    .cart-layout {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* Title, count and the way back out share one line; the count is a quiet
   second voice under the heading rather than a badge beside it. */
.cart-head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1rem 2rem;
    flex-wrap: wrap;
}

.cart-head h1 {
    margin-bottom: 0;
}

.cart-head__count {
    margin: 0.4rem 0 0;
    font-size: 0.9375rem;
    color: var(--ink-faint);
}

.cart-panel {
    background: var(--paper-raised);
    border: 1px solid var(--rule);
    border-radius: var(--radius-control);
    padding: clamp(1rem, 2.4vw, 1.75rem) clamp(1.1rem, 3vw, 2rem);
}

/* The panel supplies the outer padding, so the first and last lines must not
   pay for it twice, and the last line's rule would double the foot's. */
.cart-panel .cart-line:first-of-type {
    padding-top: 0;
}

.cart-panel .cart-line:last-of-type {
    padding-bottom: 0;
    border-bottom: 0;
}

.cart-foot {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-top: 1.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--rule);
}

.cart-notice {
    padding: clamp(1.5rem, 5vw, 2.75rem) 1rem;
    text-align: center;
    color: var(--ink-soft);
}

.cart-notice p {
    margin: 0;
}

.cart-notice p+p {
    margin-top: 0.75rem;
}

.cart-line {
    display: grid;
    grid-template-columns: 110px minmax(0, 1fr) auto;
    gap: 1.25rem;
    padding: 1.5rem 0;
    border-bottom: 1px solid var(--rule);
    align-items: start;
}

.cart-line__image {
    aspect-ratio: 1;
    background: var(--paper-sunken);
    overflow: hidden;
}

.cart-line__image img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.cart-line__title {
    font-family: var(--font-display);
    font-size: 1.2rem;
    margin: 0 0 0.2rem;
}

.cart-line__meta {
    font-size: 0.8125rem;
    color: var(--ink-faint);
    margin-bottom: 0.75rem;
}

.cart-line__price {
    text-align: right;
    white-space: nowrap;
}

.cart-line__remove {
    border: 0;
    background: none;
    padding: 0;
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--ink-faint);
    cursor: pointer;
}

.cart-line__remove:hover {
    color: var(--danger);
}

@media (max-width: 560px) {
    .cart-line {
        grid-template-columns: 84px minmax(0, 1fr);
    }

    .cart-line__price {
        grid-column: 2;
        text-align: left;
    }

    /* Nothing here earns a row of its own next to a heading that has already
       wrapped, so the head stacks and the button sits under its own count. */
    .cart-head {
        flex-direction: column;
        align-items: flex-start;
    }

    .cart-foot {
        flex-direction: column;
        align-items: flex-start;
        gap: 0.9rem;
    }
}

.summary {
    background: var(--paper-sunken);
    padding: clamp(1.5rem, 3vw, 2rem);
    position: sticky;
    top: calc(var(--header-h) + 1.5rem);
}

.summary__row {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.55rem 0;
    font-size: 0.9375rem;
}

.summary__row--total {
    border-top: 1px solid var(--rule-strong);
    margin-top: 0.6rem;
    padding-top: 1rem;
    font-family: var(--font-display);
    font-size: 1.35rem;
}

.summary .btn {
    margin-top: 1.25rem;
}

/* Scoped to the cart: checkout uses .summary too, and its panel sits in a
   column of its own where a border would only add noise. */
.cart-layout .summary {
    border: 1px solid var(--rule);
    border-radius: var(--radius-control);
}

.summary__head {
    font-family: var(--font-display);
    font-size: 1.3rem;
    margin: 0 0 1rem;
}

/* =========================================================================== 
   CONTACT — contact.php
   ---------------------------------------------------------------------------
   The layout used to be an inline style on the wrapper, which meant it had no
   media query and could not collapse: on a 440px phone the aside still claimed
   its minmax(260px, .7fr), leaving the form roughly 110px wide and every input
   narrower than the word inside it. It is a class now, for that reason alone.
   =========================================================================== */
.contact-head h1 {
    margin-bottom: 0;
}

.contact-head .lede {
    /* h1 carries 0.5em, which at a 4.5rem display size is 36px before this one
       is added — the pair read as two separate blocks rather than a title and
       its sentence. The lede owns the whole gap. */
    margin-top: 1.1rem;
    max-width: 46ch;
}

.contact-section {
    padding-bottom: clamp(3rem, 6vw, 5rem);
}

.contact-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.75fr);
    gap: clamp(1.5rem, 4vw, 3.5rem);
    align-items: start;
}

/* The form is the page. It reads as a sheet of paper laid on the ground rather
   than as loose fields, which is the same treatment the cart's order panel and
   the artwork page's buy panel get. */
.contact-panel {
    background: var(--paper-raised);
    border: 1px solid var(--rule);
    border-radius: var(--radius-control);
    padding: clamp(1.25rem, 3vw, 2rem);
}

.contact-panel .field:last-of-type {
    margin-bottom: 0;
}

/* .field carries its own bottom margin and .field-row adds a grid gap on top of
   it, so once the row stacks on a phone its two fields sat 2.25rem apart while
   every field below them sat at 1.25rem. The row owns the spacing between its
   own children; the children do not. (The same double-count is in .field-row
   wherever else it is used — scoped here rather than changed under checkout and
   booking without looking at them.) */
.contact-panel .field-row {
    margin-bottom: 1.25rem;
    /* .field-row's own 1rem gap is 4px short of the 1.25rem every other field
       carries, which showed as a stacked pair sitting tighter than the fields
       under it. Only the row axis: side by side, 1rem between the columns is
       right. */
    row-gap: 1.25rem;
}

.contact-panel .field-row .field {
    margin-bottom: 0;
}

.contact-panel textarea {
    min-height: 160px;
}

/* The button and the note it belongs with, divided from the fields above. */
.contact-submit {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem 1.5rem;
    flex-wrap: wrap;
    margin-top: 1.5rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--rule);
}

.contact-submit p {
    margin: 0;
    max-width: 26ch;
}

/* The studio card. Sunken rather than raised, so it reads as reference beside
   the form rather than as a second thing to fill in. */
.contact-aside {
    background: var(--paper-sunken);
    border: 1px solid var(--rule);
    border-radius: var(--radius-control);
    padding: clamp(1.25rem, 3vw, 1.75rem);
    position: sticky;
    top: calc(var(--header-h) + 1.5rem);
}

.contact-facts {
    list-style: none;
    margin: 0 0 1.5rem;
    padding: 0;
    display: grid;
    gap: 1.1rem;
}

/* Stacked, not a label/value row. An address runs to three lines and an email
   is 20 characters, so a two-column row either wraps raggedly or forces the
   card wider than the column it lives in. */
.contact-facts__label {
    display: block;
    font-size: 0.625rem;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin-bottom: 0.3rem;
}

.contact-facts__value {
    display: block;
    font-size: 0.9375rem;
    line-height: 1.5;
    color: var(--ink);
    /* an email has no spaces to break at, and must not push the card open */
    overflow-wrap: anywhere;
}

a.contact-facts__value {
    border-bottom: 1px solid var(--rule-strong);
    padding-bottom: 1px;
    transition: color var(--quick) var(--ease), border-color var(--quick) var(--ease);
}

a.contact-facts__value:hover {
    color: var(--accent-deep);
    border-color: var(--accent-deep);
}

/* The acknowledgement, which is the whole panel once the form has gone. */
.contact-sent {
    text-align: center;
    padding: clamp(1.5rem, 5vw, 3rem) 1rem;
}

.contact-sent__mark {
    display: grid;
    place-items: center;
    width: 58px;
    height: 58px;
    margin: 0 auto 1.25rem;
    border: 1px solid var(--rule-strong);
    border-radius: 50%;
    color: var(--accent-deep);
}

.contact-sent h2 {
    font-size: clamp(1.6rem, 3vw, 2.1rem);
    margin-bottom: 0.5rem;
}

.contact-sent p {
    margin-inline: auto;
    margin-bottom: 1.75rem;
    max-width: 34ch;
    color: var(--ink-soft);
}

@media (max-width: 900px) {
    .contact-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Nothing to stick to once it is a band under the form. */
    .contact-aside {
        position: static;
    }
}

@media (max-width: 560px) {
    /* Full width, because the row it shared with the note has stacked and a
       2rem-padded pill floating on the left reads as unfinished. */
    .contact-submit {
        flex-direction: column;
        align-items: stretch;
        text-align: center;
    }

    .contact-submit .btn {
        width: 100%;
    }

    .contact-submit p {
        max-width: none;
    }
}

/* --------------------------------------------------------------- booking */
.booking-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
    gap: clamp(2rem, 5vw, 4rem);
    align-items: start;
}

@media (max-width: 900px) {
    .booking-layout {
        grid-template-columns: 1fr;
    }
}

/* ------------------------------------------------------------ checkout */
.checkout-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.3fr) minmax(320px, 0.75fr);
    gap: clamp(2rem, 5vw, 4.5rem);
    align-items: start;
}

@media (max-width: 900px) {
    .checkout-layout {
        grid-template-columns: 1fr;
    }
}

.step-head {
    display: flex;
    align-items: baseline;
    gap: 0.85rem;
    margin-bottom: 1.5rem;
    padding-bottom: 0.75rem;
}

.step-head__num {
    font-family: var(--font-display);
    font-size: 1.4rem;
    color: var(--ink-faint);
}

/* ------------------------------------------------------------- mockups */
.mockup-strip {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
    gap: 1rem;
}

.mockup-strip figure {
    margin: 0;
}

.mockup-strip img {
    width: 100%;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    background: var(--paper-sunken);
}

.mockup-strip figcaption {
    font-size: 0.78rem;
    color: var(--ink-faint);
    padding-top: 0.5rem;
}

/* --------------------------------------------------------- testimonials */
.quote-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
    gap: clamp(1.5rem, 3vw, 2.5rem);
}

.quote {
    padding: clamp(1.5rem, 3vw, 2.25rem);
    background: var(--paper-raised);
    border: 1px solid var(--rule);
}

.quote blockquote {
    margin: 0 0 1.25rem;
    font-family: var(--font-display);
    font-size: 1.2rem;
    line-height: 1.5;
}

.quote figcaption {
    font-size: 0.8125rem;
    color: var(--ink-faint);
}

.quote__stars {
    color: var(--accent);
    letter-spacing: 0.1em;
    margin-bottom: 0.75rem;
    font-size: 0.85rem;
}

/* ----------------------------------------------------------- newsletter */
.newsletter-form {
    display: flex;
    gap: 0.5rem;
    max-width: 480px;
}

.newsletter-form input {
    flex: 1;
    border: 0;
    border-bottom: 1px solid currentColor;
    background: transparent;
    padding: 0.75rem 0;
    font-family: var(--font-body);
    font-size: 0.95rem;
    color: inherit;
}

.newsletter-form input::placeholder {
    color: currentColor;
    opacity: 0.5;
}

.newsletter-form input:focus {
    outline: none;
}

@media (max-width: 560px) {
    .newsletter-form {
        flex-direction: column;
    }
}

/* --------------------------------------------------------------- footer */
/* Deeper than the page rather than lighter — on a dark ground a footer that
   lifts reads as a panel that floated loose, where one that recedes reads as
   the edge of the page. */
.site-footer {
    background: var(--ink-surface);
    color: var(--ink-surface-text);
    padding-block: clamp(3rem, 6vw, 6.75rem) 2rem;
}

.site-footer a {
    color: var(--ink-surface-text);
    transition: color var(--quick) var(--ease);
}

.site-footer a:hover {
    /* The footer is an ink surface. Under light, --ink is that same near-black,
       so hovering a link used to make it disappear. */
    color: var(--ink-surface-strong);
}

.footer-grid {
    display: grid;
    grid-template-columns: 1.4fr repeat(3, 1fr);
    gap: clamp(2rem, 4vw, 3.5rem);
    padding-bottom: 3rem;
}

@media (max-width: 860px) {
    .footer-grid {
        grid-template-columns: 1fr 1fr;
    }
}

@media (max-width: 520px) {
    .footer-grid {
        grid-template-columns: 1fr;
    }
}

/* The footer wordmark carries the mark as well as the name, so it becomes a row
   rather than a line of text. text-indent goes with it: on .wordmark that .34em
   only exists to balance the trailing letter-space under a centred lockup, and
   here it would just shove the mark off its own left edge.

   The asset is a square lockup that includes its own "BLVCK MILK" line, which
   next to the name beside it would say the name twice — so the box crops to the
   face mark alone. The three multiples below are that mark's rect inside the
   4167px source, and they are the same numbers the film page's top bar uses. */
.footer-wordmark {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    margin-bottom: 1rem;
    text-indent: 0;
    color: var(--ink-surface-strong);
}

.footer-wordmark .wm-mark {
    --d: 30px;
    position: relative;
    flex: 0 0 auto;
    width: var(--d);
    height: var(--d);
    overflow: hidden;
}

.footer-wordmark .wm-mark img {
    position: absolute;
    /* the global img rule caps this at 100%, which would fit the whole lockup
       into the box instead of cropping to the part of it we want */
    max-width: none;
    width: calc(var(--d) * 2.341);
    left: calc(var(--d) * -0.6705);
    top: calc(var(--d) * -0.5492);
}

.footer-title {
    font-size: 0.6875rem;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-surface-strong);
    margin-bottom: 1.1rem;
}

.footer-links {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 0.55rem;
    font-size: 0.875rem;
}

/* The group headings are <summary> elements now — see the note in footer.php.
   Above phone width app.js pins every group open, so the heading must not read
   as a control: no disclosure triangle, no chevron, no pointer. The phone block
   near the foot of this file turns all three back on. */
.footer-col > summary {
    list-style: none;
    cursor: default;
}

.footer-col > summary::-webkit-details-marker {
    display: none;
}

.footer-col__chev {
    display: none;
}

/* One line of copyright, centred. It sat opposite the studio email until that
   was dropped — reaching the studio goes through the Contact link above — and
   a lone item pushed to the left of a wide rule reads as though something fell
   off the other end. */
.footer-bottom {
    display: flex;
    justify-content: center;
    gap: 1.5rem;
    flex-wrap: wrap;
    padding-top: 1.75rem;
    font-size: 0.8125rem;
    text-align: center;
}

/* The names were spelled out here until the row read as a fifth column of
   footer links. The marks carry it instead — each one keeps a real tap target
   and its name in the accessible tree, so nothing is lost by dropping the
   word. Squared, like everything else on this page. */
/* Divided from the account links above it. This used to be an inline
   margin-top on the heading; wrapping the pair in .footer-social to reposition
   it on a phone took that with it, and on a wide screen "Elsewhere" then sat
   flush against "Cart" as though it were one more link in the list. */
.footer-social {
    margin-top: 2rem;
}

.social-links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    font-size: 1.1rem;
}

.social-links a {
    display: inline-grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 1px solid var(--rule);
    transition: color var(--quick) var(--ease), border-color var(--quick) var(--ease);
}

.social-links a:hover {
    /* --accent-deep is tuned against the page, not against this near-black
       footer, where under light it falls to about 1.6:1. */
    color: var(--ink-surface-accent);
    border-color: var(--rule-strong);
}

/* Neither .btn (fills with --ink) nor .btn--light (fills with --paper) lands on
   an inverted surface in both themes — one of the two is always the colour of
   the panel itself, and on this one the fill measured 1.05:1 against its own
   ground. Painted from the surface's own tokens it reads in either. */
.site-footer .newsletter-form .btn {
    background: var(--ink-surface-strong);
    border-color: var(--ink-surface-strong);
    color: var(--ink-surface);
}

.site-footer .newsletter-form .btn:hover {
    /* The surface's own accent, not the page's: --accent-deep is a dark brown
       under light, and dark brown behind --ink-surface text is barely 1.5:1. */
    background: var(--ink-surface-accent);
    border-color: var(--ink-surface-accent);
    color: var(--ink-surface);
}

/* -------------------------------------------------------------- notices */
.flash-stack {
    position: fixed;
    top: calc(var(--header-h) + 0.75rem);
    right: calc(var(--gutter) + env(safe-area-inset-right));
    z-index: 120;
    display: grid;
    gap: 0.6rem;
    max-width: min(380px, calc(100vw - 2 * var(--gutter)));
}

.flash {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.95rem 1.1rem;
    background: var(--paper-raised);
    border-left: 2px solid var(--ink);
    box-shadow: var(--shadow-lift);
    font-size: 0.875rem;
    animation: flash-in var(--slow) var(--ease);
}

.flash--success {
    border-left-color: var(--success);
}

.flash--danger {
    border-left-color: var(--danger);
}

.flash--warning {
    border-left-color: var(--accent);
}

.flash button {
    margin-left: auto;
    border: 0;
    background: none;
    cursor: pointer;
    color: var(--ink-faint);
}

@keyframes flash-in {
    from {
        opacity: 0;
        transform: translateX(16px);
    }

    to {
        opacity: 1;
        transform: translateX(0);
    }
}

.empty-state {
    text-align: center;
    padding: clamp(3rem, 8vw, 6rem) 1rem;
    color: var(--ink-faint);
}

.empty-state h3 {
    color: var(--ink);
}

.empty-state p {
    margin-inline: auto;
}

/* ----------------------------------------------------------- utilities */
.stack>*+* {
    margin-top: var(--stack-gap, 1rem);
}

.center {
    text-align: center;
}

.center p {
    margin-inline: auto;
}

.flow-tight {
    --stack-gap: 0.5rem;
}

.nowrap {
    white-space: nowrap;
}

.visually-hidden {
    position: absolute !important;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

/* Reveal-on-scroll. The no-js and reduced-motion cases both show everything
   immediately, so content is never hidden behind an animation that never runs. */
.reveal {
    opacity: 0;
    transform: translateY(18px);
}

.reveal.is-visible {
    opacity: 1;
    transform: none;
    transition: opacity var(--slow) var(--ease), transform var(--slow) var(--ease);
}

.no-js .reveal {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    .reveal {
        opacity: 1;
        transform: none;
    }
}

/* An image that has not decoded yet shows the sunken ground rather than a hole
   in the page. Keyed off the frames and not off loading="lazy", because the
   above-the-fold cards are deliberately eager and would otherwise lose it. */
.art-card__frame img,
img[loading="lazy"] {
    background: var(--paper-sunken);
}

/* ===========================================================================
   RESPONSIVE — phones and small tablets
   ---------------------------------------------------------------------------
   Everything above is written mobile-tolerant, but a few things only become
   problems once the viewport is narrower than the header's natural width or
   the pointer stops being a mouse. Those two cases are handled here.
   =========================================================================== */

/* ------------------------------------------------- header below the burger */
@media (max-width: 960px) {
    :root {
        --header-h: 64px;
    }

    .site-header__inner {
        gap: 0.75rem;
    }

    /* The desktop wordmark is 187px of letter-spacing, which alone eats half
       of a 360px phone. Scale it with the viewport instead. */
    .wordmark {
        font-size: clamp(0.95rem, 3.6vw, 1.4rem);
        letter-spacing: clamp(0.16em, 0.9vw, 0.34em);
        text-indent: clamp(0.16em, 0.9vw, 0.34em);
    }

    .header-actions {
        gap: 0.15rem;
    }

    .icon-btn {
        width: 42px;
        height: 42px;
    }
}

/* ----------------------------------------------- header icons on phones */
/* Saved artwork and account each have an entry in the burger menu, and search
   is carried by the home screen's and the shop's own fields. Repeating any of
   them in the header puts the same destination on screen twice and crowds the
   wordmark, so the header keeps only the cart. Note that this leaves pages
   other than home and shop with no way to reach search on a phone. */
@media (max-width: 700px) {
    .site-header .icon-btn--search,
    .site-header .icon-btn--wishlist,
    .site-header .icon-btn--account,
    .site-header .icon-btn--theme {
        display: none;
    }

    /* What is left is the cart and the menu, and the softened tone the icons
       take on desktop was only ever there to keep them level with the nav
       links. With no links beside them, soft just reads as faint — and the
       cart is the one commerce control on the screen. */
    .site-header .icon-btn {
        color: var(--ink);
    }
}

/* -------------------------------------------------------------- mobile nav */
@media (max-width: 960px) {
    .mobile-nav {
        padding-top: calc(1.5rem + env(safe-area-inset-top));
        padding-bottom: calc(3rem + env(safe-area-inset-bottom));
        /* Fills the visible area on a phone browser whose toolbars collapse. */
        height: 100dvh;
    }

    .mobile-nav__head {
        min-height: var(--header-h);
    }

    .mobile-nav__links a,
    .mobile-nav__theme {
        font-size: clamp(1.5rem, 6vw, 2rem);
        padding-block: 0.6rem;
    }
}

/* ------------------------------------------------------------- rhythm */
/* The desktop section rhythm is written for a page two-and-a-half times this
   wide. Kept at full size on a phone it stops reading as breathing room and
   starts reading as a gap between unrelated things — a block, a void, another
   block. Sections are set closer here so the page reads as one sequence. */
@media (max-width: 700px) {
    .section {
        padding-block: clamp(2.5rem, 9vw, 3.25rem);
    }

    .section--tight {
        padding-block: clamp(2rem, 7vw, 2.75rem);
    }
}

/* ---------------------------------------------------------------- notices */
@media (max-width: 600px) {
    .flash-stack {
        left: calc(var(--gutter) + env(safe-area-inset-left));
        right: calc(var(--gutter) + env(safe-area-inset-right));
        max-width: none;
    }
}

/* ------------------------------------------------------------------ hero */
@media (max-width: 600px) {

    /* Caption spans the plate rather than stopping three-quarters across,
       which on a narrow figure reads as a mistake. */
    .hero__caption {
        left: 0;
        right: 0;
        max-width: none;
        padding: 1rem 1.15rem;
    }
}

@media (max-width: 480px) {

    /* Two buttons of different word-lengths look ragged when they wrap.
       Stacked full-width, they read as a deliberate pair. */
    .hero__actions {
        flex-direction: column;
        align-items: stretch;
    }

    .hero__actions .btn {
        width: 100%;
    }
}

/* --------------------------------------------------------- artwork detail */
@media (max-width: 1000px) {

    /* No longer a column beside the image, so it should not also be a
       sticky panel fighting the page scroll. */
    .summary {
        position: static;
    }
}

@media (max-width: 900px) {
    .facets {
        position: static;
    }
}

/* --------------------------------------------------------------- filters */
@media (max-width: 600px) {
    .filter-bar {
        gap: 0.75rem 1rem;
    }

    .option-tabs {
        flex-wrap: wrap;
    }

    .option-tab {
        flex: 1 0 45%;
    }

    .size-option {
        flex-wrap: wrap;
        gap: 0.25rem 1rem;
    }
}

/* ----------------------------------------------------------------- forms */
@media (max-width: 900px) {

    /* Safari zooms the whole page when a focused control is under 16px, and
       never zooms back out. Every text control therefore gets 16px on touch
       sizes, whatever its visual scale elsewhere. */
    .field input[type="text"],
    .field input[type="email"],
    .field input[type="tel"],
    .field input[type="password"],
    .field input[type="number"],
    .field input[type="date"],
    .field input[type="search"],
    .field select,
    .field textarea,
    .newsletter-form input,
    .price-range input,
    .select,
    .qty input {
        font-size: 16px;
    }

    .select {
        padding-block: 0.6rem;
    }
}

/* --------------------------------------------------------------- footer */
/*
 * On a phone the footer was 1273px of a 2298px page — 55% of the whole scroll,
 * and 762px of that was eighteen links stacked in one column. Here the three
 * groups become collapsed rows and the block that carries the brand and the
 * letter keeps its air, which is the part anyone actually stops on.
 */
@media (max-width: 700px) {
    .site-footer {
        padding-block: 2.25rem calc(1.25rem + env(safe-area-inset-bottom));
    }

    /* One column, and the gaps go to zero: the rows are divided by their own
       hairlines now, which is what makes a stack of collapsed groups read as a
       list rather than as four blocks that happen to be near each other. */
    .footer-grid {
        grid-template-columns: 1fr;
        gap: 0;
        padding-bottom: 0;
        border-bottom: 0;
    }

    /* Only the link groups collapse — the brand and the letter keep theirs. */
    .footer-grid > :first-child {
        padding-bottom: 1.6rem;
    }

    /* Back on one line. Stacked, the field and the button are 100px of a footer
       that is being halved, and the button only ever holds one word. */
    .site-footer .newsletter-form {
        flex-direction: row;
        align-items: center;
        gap: 0.75rem;
    }

    .site-footer .newsletter-form .btn {
        flex: none;
        padding-inline: 1.15rem;
    }

    /* Each group is one row: its name, and a chevron that turns. */
    .footer-col {
        border-top: 1px solid var(--rule);
    }

    .footer-col > summary {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        margin: 0;
        /* 44px of tap target, which is what the row is for. */
        padding-block: 1.05rem;
        cursor: pointer;
    }

    .footer-col__chev {
        display: block;
        flex: none;
        color: var(--ink-faint);
        transition: transform var(--quick) var(--ease);
    }

    .footer-col[open] > summary .footer-col__chev {
        transform: rotate(180deg);
    }

    /* Roomier than the desktop's 0.55rem: open, these are tap targets. */
    .footer-col > .footer-links {
        gap: 0.85rem;
        padding-bottom: 1.25rem;
    }

    /* Elsewhere is four marks and a word — a group of its own would be two rows
       carrying nothing. It sits on the rule under the last group instead. */
    .footer-social {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        margin-top: 0;
        padding-block: 0.75rem;
        border-top: 1px solid var(--rule);
    }

    .footer-social .footer-title {
        margin: 0;
    }

    .social-links a {
        width: 2.25rem;
        height: 2.25rem;
    }

    .footer-bottom {
        gap: 0.75rem;
        padding-top: 1.1rem;
        border-top: 1px solid var(--rule);
        font-size: 0.75rem;
        line-height: 1.55;
    }
}

/* ===========================================================================
   TOUCH — anything whose only trigger is :hover is unreachable without a mouse
   =========================================================================== */
@media (hover: none) {

    /* Save is revealed by hover alone, so on touch it would never appear at
       all. It is small enough to sit on the art permanently. */
    .save-btn {
        opacity: 1;
        transform: none;
    }

    /* Quick view is the opposite case: revealing it permanently puts a white
       pill across the middle of every card. On a tablet the cards are wide
       enough to carry it; on a phone the artwork page is one tap away and
       says more, so the shortcut is dropped rather than shown. */
    .art-card__overlay {
        opacity: 1;
    }

    .art-card__quick {
        transform: none;
    }

    /* Hover states that stick after a tap read as a stuck selection. */
    .btn:hover {
        background: var(--ink);
        border-color: var(--ink);
    }

    .btn--ghost:hover {
        background: transparent;
        color: var(--ink);
    }

    .btn--light:hover {
        background: var(--paper);
        color: var(--ink);
        border-color: var(--paper);
    }

    .art-card:hover .art-card__frame img {
        transform: none;
    }

    .icon-btn:hover {
        background: transparent;
    }

    /* Editorial links are 24px tall by default — under the thumb minimum. */
    .link-underline {
        padding-block: 0.5rem 0.35rem;
    }

    .cart-line__remove,
    .facet-option {
        min-height: 32px;
    }
}

@media (hover: none) and (max-width: 700px) {
    .art-card__overlay {
        display: none;
    }
}

/* ===========================================================================
   THE COLLECTION — shop.php
   ---------------------------------------------------------------------------
   One shell, four stacked bands: head → lenses → toolbar → results. Every band
   uses the same gutter, so the breadcrumb, the title, the tabs, the search
   field, the sidebar and the first column of artwork all hang off one left
   edge. Nothing here invents a second visual language: hairline rules, small
   caps, one accent, and the artwork carrying the colour.
   =========================================================================== */

/* ---------------------------------------------------------------- page head */
.collection-head {
    padding-block: clamp(1.75rem, 4vw, 3.25rem) clamp(1.25rem, 2.5vw, 1.75rem);
}

.crumbs {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin-bottom: clamp(0.9rem, 2vw, 1.35rem);
}

.crumbs a {
    transition: color var(--quick) var(--ease);
}

.crumbs a:hover {
    color: var(--ink);
}

.crumbs__sep {
    color: var(--rule-strong);
}

.collection-head__title {
    margin: 0;
    font-size: clamp(2.25rem, 5vw, 4rem);
    line-height: 1.02;
}

.collection-head__lede {
    margin: clamp(0.85rem, 2vw, 1.15rem) 0 0;
    /* Held to a reading measure so the sentence never runs the full 1440. */
    max-width: 54ch;
    color: var(--ink-soft);
    font-size: clamp(0.95rem, 1.3vw, 1.0625rem);
    line-height: 1.7;
}

/* --------------------------------------------------------------- lens tabs */
/* The four routes most visitors actually want, lifted out of the sidebar. */
.lens {
    border-bottom: 1px solid var(--rule);
}

.lens__track {
    display: flex;
    gap: clamp(1.25rem, 3vw, 2.5rem);
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.lens__track::-webkit-scrollbar {
    display: none;
}

.lens__tab {
    position: relative;
    flex: 0 0 auto;
    padding-block: 1.05rem;
    font-size: 0.72rem;
    font-weight: 500;
    letter-spacing: 0.17em;
    text-transform: uppercase;
    color: var(--ink-faint);
    white-space: nowrap;
    transition: color var(--quick) var(--ease);
}

/* Sits on the band's own hairline, so the active tab reads as cut out of it. */
.lens__tab::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    bottom: -1px;
    height: 1px;
    background: var(--ink);
    transform: scaleX(0);
    transform-origin: left;
    transition: transform var(--quick) var(--ease);
}

.lens__tab:hover,
.lens__tab.is-active {
    color: var(--ink);
}

.lens__tab.is-active::after {
    transform: scaleX(1);
}

/* ----------------------------------------------------------------- toolbar */
/* Sticks under the site header. z-index stays below the header's 60 so the
   two never argue about which one is on top where they overlap. */
.toolbar {
    position: sticky;
    top: calc(var(--header-h) - 1px);
    z-index: 40;
    background: var(--paper);
    border-bottom: 1px solid var(--rule);
}

.toolbar__inner {
    display: flex;
    align-items: center;
    gap: clamp(1rem, 2.5vw, 2rem);
    min-height: 66px;
}

/* A pill rather than a bare underline: on a dark ground an unbounded input
   has no edge at all, and the field stops looking like somewhere to type. */
.toolbar__search {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.7rem;
    flex: 1 1 auto;
    min-width: 0;
    max-width: 34rem;
    padding-inline: 1rem;
    border: 1px solid var(--rule);
    border-radius: 999px;
    background: var(--paper-raised);
    transition: border-color var(--quick) var(--ease);
}

.toolbar__search:focus-within {
    border-color: var(--rule-strong);
}

.toolbar__search-icon {
    flex: none;
    color: var(--ink-faint);
    transition: color var(--quick) var(--ease);
}

.toolbar__search:focus-within .toolbar__search-icon {
    color: var(--ink);
}

.toolbar__search-input {
    flex: 1 1 auto;
    min-width: 0;
    border: 0;
    background: transparent;
    padding-block: 0.65rem;
    font-family: var(--font-body);
    font-size: 0.9375rem;
    color: var(--ink);
    outline: none;
}

.toolbar__search-input::placeholder {
    color: var(--ink-faint);
}

/* The native clear affordance duplicates ours and sits at the wrong end. */
.toolbar__search-input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
}

.toolbar__search-clear {
    flex: none;
    display: grid;
    place-items: center;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--paper-sunken);
    color: var(--ink-soft);
    transition: background var(--quick) var(--ease), color var(--quick) var(--ease);
}

.toolbar__search-clear:hover {
    background: var(--ink);
    color: var(--paper);
}

.toolbar__search-clear.is-hidden {
    display: none;
}

.toolbar__tools {
    display: flex;
    align-items: center;
    gap: clamp(0.75rem, 1.5vw, 1.25rem);
    flex: none;
}

/* Phone only — see the phone block near the foot of this file. */
.toolbar__section,
.facet--sort {
    display: none;
}

.toolbar__btn {
    display: none;
    align-items: center;
    gap: 0.5rem;
    border-radius: 999px;
    border: 1px solid var(--rule);
    background: var(--paper-raised);
    color: var(--ink);
    font-family: var(--font-body);
    font-size: 0.7rem;
    font-weight: 500;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    padding: 0.7rem 1.1rem;
    cursor: pointer;
    transition: border-color var(--quick) var(--ease), background var(--quick) var(--ease);
}

.toolbar__btn:hover {
    border-color: var(--ink);
}

.toolbar__btn-count {
    display: grid;
    place-items: center;
    min-width: 18px;
    height: 18px;
    padding-inline: 0.3rem;
    background: var(--accent-deep);
    color: var(--paper);
    font-size: 0.625rem;
    letter-spacing: 0;
    border-radius: 9px;
}

.toolbar__sort {
    position: relative;
    display: flex;
    align-items: center;
    gap: 0.6rem;
}

.toolbar__sort label {
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.toolbar__select {
    appearance: none;
    -webkit-appearance: none;
    border: 0;
    background: transparent;
    padding: 0.35rem 1.4rem 0.35rem 0;
    font-family: var(--font-body);
    font-size: 0.8125rem;
    color: var(--ink);
    cursor: pointer;
    outline-offset: 4px;
}

.toolbar__sort-caret {
    position: absolute;
    right: 0.15rem;
    color: var(--ink-faint);
    pointer-events: none;
}

.view-switch {
    display: inline-flex;
    border: 1px solid var(--rule);
    border-radius: var(--radius-control);
    overflow: hidden;
}

.view-switch__btn {
    display: grid;
    place-items: center;
    width: 38px;
    height: 38px;
    border: 0;
    background: transparent;
    color: var(--ink-faint);
    cursor: pointer;
    transition: background var(--quick) var(--ease), color var(--quick) var(--ease);
}

.view-switch__btn + .view-switch__btn {
    border-left: 1px solid var(--rule);
}

.view-switch__btn:hover {
    color: var(--ink);
}

.view-switch__btn.is-active {
    background: var(--ink);
    color: var(--paper);
}

/* ---------------------------------------------------------- applied filters */
.applied {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5rem 0.6rem;
    padding-block: 1.15rem;
    border-bottom: 1px solid var(--rule);
}

.applied__label {
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink-faint);
    margin-right: 0.25rem;
}

.applied__chip {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.8rem;
    border-radius: 999px;
    border: 1px solid var(--rule);
    background: var(--paper-raised);
    font-size: 0.75rem;
    color: var(--ink-soft);
    transition: border-color var(--quick) var(--ease), color var(--quick) var(--ease);
}

.applied__chip svg {
    color: var(--ink-faint);
    transition: color var(--quick) var(--ease);
}

.applied__chip:hover {
    border-color: var(--ink);
    color: var(--ink);
}

.applied__chip:hover svg {
    color: var(--danger);
}

.applied__clear {
    margin-left: auto;
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink-faint);
    border-bottom: 1px solid var(--rule-strong);
    padding-bottom: 2px;
    transition: color var(--quick) var(--ease), border-color var(--quick) var(--ease);
}

.applied__clear:hover {
    color: var(--ink);
    border-color: var(--ink);
}

/* ----------------------------------------------------------------- results */
.collection {
    display: grid;
    grid-template-columns: 256px minmax(0, 1fr);
    gap: clamp(2rem, 4vw, 3.5rem);
    align-items: start;
    padding-block: clamp(1.75rem, 3.5vw, 2.75rem) clamp(4rem, 8vw, 7rem);
}

.collection__aside {
    position: sticky;
    /* Header + toolbar, so the sidebar never slides under either. */
    top: calc(var(--header-h) + 66px + 1.5rem);
    /* The facet list is longer than most viewports, so it scrolls on its own
       instead of forcing the page down past the last row of artwork. */
    max-height: calc(100vh - var(--header-h) - 66px - 3rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    min-width: 0;
    /* Room for the scrollbar so option counts do not sit under it. */
    padding-right: 0.5rem;
    margin-right: -0.5rem;
}

.collection__aside-head,
.collection__aside-foot {
    display: none;
}

.collection__results {
    min-width: 0;
}

.collection__meta {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    /* Aligns with the sidebar's first facet label rather than floating above it. */
    padding-bottom: 1.1rem;
    margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
    font-size: 0.75rem;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.collection__meta-page {
    flex: none;
}

/*
 * Grid view: a justified mosaic. Every row is filled edge to edge, the tiles
 * come out at different widths and different heights, and every piece is at
 * its own true proportions — nothing is trimmed to make the rows line up.
 *
 * The arithmetic is flexbox's, not a script's. Each tile's basis AND its grow
 * factor are its own aspect ratio, so flex settles the row at
 *
 *     width = ratio x k
 *
 * and every height — width / ratio — lands on the same k. Level rows, true
 * shapes. --ar is published by the card partial from the file's real pixels.
 *
 * This replaces the square mat. The mat existed so a piece could be shown
 * whole instead of cropped to a common frame; a layout built from the pieces'
 * own ratios reaches the same end without spending a fifth of every tile on
 * padding, so the intent survives and the artwork gets the room.
 */
.collection__grid[data-view-mode="grid"] .art-grid {
    display: flex;
    flex-wrap: wrap;
    gap: clamp(0.5rem, 1vw, 0.85rem);
}

.collection__grid[data-view-mode="grid"] .art-card {
    position: relative;
    /* 17rem is the row's nominal height before flex justifies it. */
    flex: var(--ar, 1) 1 calc(var(--ar, 1) * 17rem);
    min-width: 0;
}

/*
 * The matted square is still the shared base — LIST view depends on it, where
 * a thumbnail beside a row of text does want a common frame and the whole
 * piece inside it. Only grid view is remade as the mosaic, below.
 */
.collection__grid .art-card__frame--ratio {
    aspect-ratio: 1 / 1;
    border-radius: var(--radius-art);
    display: grid;
    place-items: center;
    padding: clamp(0.75rem, 2vw, 1.4rem);
    background: var(--paper-sunken);
}

.collection__grid .art-card__frame--ratio img {
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    /* A mounted print casts a shadow; a flat one looks like a placeholder. */
    box-shadow: 0 1px 2px rgba(23, 22, 19, 0.08), 0 10px 26px rgba(23, 22, 19, 0.10);
}

.collection__grid .art-card__frame--ratio picture {
    display: contents;
}

/* Grid view only: the tile is the piece's own shape, edge to edge. */
.collection__grid[data-view-mode="grid"] .art-card__frame--ratio {
    aspect-ratio: var(--ar, 1);
    display: block;
    padding: 0;
}

.collection__grid[data-view-mode="grid"] .art-card__frame--ratio img {
    width: 100%;
    height: 100%;
    max-width: none;
    max-height: none;
    /* The box is the file's own ratio, so cover and contain agree here and
       nothing is really cut; cover only guards a thumbnail regenerated at a
       shape the row was not solved for. */
    object-fit: cover;
    box-shadow: none;
}

.collection__grid[data-view-mode="grid"] .art-card__frame--ratio picture {
    display: block;
    width: 100%;
    height: 100%;
}

/*
 * The caption rides the foot of the picture. Stacked underneath it would make
 * each card taller than its tile by a different amount — captions are one or
 * two lines depending on the piece — and the mosaic's rows would stop being
 * flush, which is the whole point of justifying them.
 */
/* UNDER the picture, not over it.
   This was absolutely positioned across the foot of every tile on a dark
   scrim that ramped to 90% opacity — measured, it covered roughly the bottom
   40% of each painting, which is a strange thing for a gallery to do to the
   work it exists to show. A visitor could not see the lower third of a piece
   without opening it.
   Now it sits below the frame on the page's own ground, so the artwork is
   never covered. The cost is honest and small: rows are a caption taller. */
.collection__grid[data-view-mode="grid"] .art-card__body {
    position: static;
    padding: 0.85rem 0 0;
    background: none;
    border-radius: 0;
    pointer-events: auto;
}

/* Back to page ink. These three took --ink-surface-* because they used to sit
   on a dark scrim in both themes; on the page they follow the theme like
   everything else, and keeping the surface tokens would have put near-white
   type on paper under the light theme. */
.collection__grid[data-view-mode="grid"] .art-card__title {
    margin: 0;
    font-size: 1.05rem;
    line-height: 1.2;
    color: inherit;
}

.collection__grid[data-view-mode="grid"] .art-card__meta {
    margin: 0.15rem 0 0;
    font-size: 0.75rem;
    color: inherit;
    opacity: 0.7;
}

.collection__grid[data-view-mode="grid"] .art-card__price {
    margin: 0.35rem 0 0;
    font-weight: 600;
    color: inherit;
}

/* The mat is only ~22px deep, so an inset label would land on the artwork.
   Both controls sit in the frame's corner instead, on the mat itself. */
.collection__grid .art-status {
    /* Inset from the corner now the frame is rounded — at 0 the pill's corner
       escapes the curve and hangs in the gutter. */
    top: 0.6rem;
    left: 0.6rem;
    /* The default cap wraps "Original — 1 only" onto two lines on a phone,
       which then covers a third of the artwork. One line, always. */
    max-width: none;
    white-space: nowrap;
}

.collection__grid .save-btn {
    top: 0.35rem;
    right: 0.35rem;
}

/* List view: one row per piece, image left, everything else on a baseline. */
.collection__grid[data-view-mode="list"] .art-grid {
    grid-template-columns: 1fr;
    gap: 0;
}

.collection__grid[data-view-mode="list"] .art-card {
    display: grid;
    grid-template-columns: 128px minmax(0, 1fr) auto;
    align-items: center;
    gap: clamp(1.25rem, 3vw, 2.25rem);
    padding-block: 1.25rem;
    border-bottom: 1px solid var(--rule);
}

/* The card's three children are placed explicitly. Left to auto-placement,
   the save button's own grid-column pushes the body onto a second row. */
.collection__grid[data-view-mode="list"] .art-card__frame {
    grid-column: 1;
    grid-row: 1;
}

.collection__grid[data-view-mode="list"] .art-card__frame--ratio {
    aspect-ratio: 1 / 1;
    padding: 0.6rem;
}

.collection__grid[data-view-mode="list"] .art-card__body {
    grid-column: 2;
    grid-row: 1;
    padding-top: 0;
}

.collection__grid[data-view-mode="list"] .art-card__title {
    font-size: 1.35rem;
}

.collection__grid[data-view-mode="list"] .art-card__price {
    margin-top: 0.35rem;
}

/* Static in the row, so it does not sit on top of the artwork. */
.collection__grid[data-view-mode="list"] .save-btn {
    position: static;
    grid-column: 3;
    grid-row: 1;
    opacity: 1;
    transform: none;
    background: transparent;
    border: 1px solid var(--rule-strong);
    border-radius: 0;
    width: 42px;
    height: 42px;
}

.collection__grid[data-view-mode="list"] .art-status {
    top: 0.4rem;
    left: 0.4rem;
    font-size: 0.5rem;
    letter-spacing: 0.1em;
    padding: 0.2rem 0.4rem;
}

.collection__grid[data-view-mode="list"] .art-card__overlay {
    display: none;
}

/* Scrim behind the phone filter sheet. */
.collection__scrim {
    position: fixed;
    inset: 0;
    z-index: 110;
    background: rgba(0, 0, 0, 0.66);
    border: 0;
    padding: 0;
}

.collection__scrim[hidden] {
    display: none;
}

body.is-locked {
    overflow: hidden;
}

/* ------------------------------------------------------- narrower desktops */
@media (max-width: 1200px) {
    .collection {
        grid-template-columns: 224px minmax(0, 1fr);
    }

    /* A narrower results column wants a shorter nominal row, or the mosaic
       drops to two very large pieces per row. */
    .collection__grid[data-view-mode="grid"] .art-card {
        flex-basis: calc(var(--ar, 1) * 14rem);
    }
}

/* ------------------------------------------- tablets and phones: one column */
@media (max-width: 900px) {
    .collection {
        grid-template-columns: 1fr;
        gap: 0;
        padding-top: clamp(1.25rem, 3vw, 1.75rem);
    }

    /* The sidebar becomes a sheet that slides in over the grid. Its desktop
       scroll settings are undone here: the sheet is full height and it is
       .collection__aside-scroll, not the panel, that scrolls. */
    .collection__aside {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: auto;
        z-index: 120;
        max-height: none;
        overflow: hidden;
        padding-right: 0;
        margin-right: 0;
        width: min(380px, 88vw);
        display: grid;
        grid-template-rows: auto minmax(0, 1fr) auto;
        background: var(--paper);
        border-left: 1px solid var(--rule);
        box-shadow: var(--shadow-frame);
        transform: translateX(101%);
        visibility: hidden;
        transition: transform var(--slow) var(--ease), visibility var(--slow);
    }

    .collection__aside.is-open {
        transform: translateX(0);
        visibility: visible;
    }

    .collection__aside-head {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        padding: 1.25rem 1.35rem;
        border-bottom: 1px solid var(--rule);
    }

    .collection__aside-title {
        font-size: 0.6875rem;
        font-weight: 500;
        letter-spacing: 0.2em;
        text-transform: uppercase;
        color: var(--ink-faint);
    }

    .collection__aside-close {
        display: grid;
        place-items: center;
        width: 40px;
        height: 40px;
        margin-right: -0.5rem;
        border: 0;
        background: transparent;
        color: var(--ink);
        cursor: pointer;
    }

    .collection__aside-scroll {
        overflow-y: auto;
        -webkit-overflow-scrolling: touch;
        padding: 0.5rem 1.35rem 1.35rem;
    }

    .collection__aside-foot {
        display: block;
        padding: 0.85rem 1.35rem calc(1rem + env(safe-area-inset-bottom));
        border-top: 1px solid var(--rule);
        background: var(--paper);
    }

    .collection__aside-count {
        margin: 0 0 0.7rem;
        font-size: 0.6875rem;
        font-weight: 500;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        text-align: center;
        color: var(--ink-faint);
    }

    /* Inside the sheet the form's own submit button is redundant. */
    .collection__aside .facets {
        position: static;
    }

    .toolbar__btn {
        display: inline-flex;
    }

    .toolbar__sort label {
        display: none;
    }
}

/* --------------------------------------------------------------- phones */
@media (max-width: 700px) {
    .toolbar__inner {
        flex-wrap: wrap;
        gap: 0;
        padding-block: 0.35rem 0.75rem;
    }

    .toolbar__search {
        order: 1;
        width: 100%;
        padding-block: 0.35rem;
        border-bottom: 1px solid var(--rule);
    }

    .toolbar__search-input {
        /* Safari zooms the page for anything under 16px and never zooms back. */
        font-size: 16px;
    }

    .toolbar__tools {
        order: 2;
        width: 100%;
        justify-content: space-between;
        padding-top: 0.75rem;
    }

    .collection__meta {
        font-size: 0.6875rem;
        padding-bottom: 0.9rem;
        margin-bottom: 1.25rem;
    }

    .collection__grid[data-view-mode="grid"] .art-grid {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 1.5rem 1rem;
    }

    /* ------------------------------------------------- list view, phone */
    /*
     * A row per piece, as a card rather than a hairline-ruled table row.
     *
     * The old layout gave the save control a grid row of its own under the
     * caption, which made every row taller than the thumbnail beside it and
     * put a 42px square outline in the middle of a list of paintings. Here it
     * is a small circle on the title's line, where it reads as an action on
     * that piece rather than an object in the list.
     *
     * One row, three columns: thumbnail, caption, save. The caption keeps its
     * own block flow rather than being dissolved onto the card's grid — with
     * display:contents its three lines became three grid rows, and the 84px
     * thumbnail spanning them had its extra height shared out between them,
     * which is what pushed the medium and the price apart.
     */
    .collection__grid[data-view-mode="list"] .art-grid {
        gap: 0.7rem;
    }

    .collection__grid[data-view-mode="list"] .art-card {
        grid-template-columns: 84px minmax(0, 1fr) auto;
        grid-template-areas: none;
        align-items: start;
        column-gap: 0.9rem;
        row-gap: 0;
        padding: 0.8rem;
        border: 1px solid var(--rule);
        border-bottom: 1px solid var(--rule);
        border-radius: 16px;
        background: var(--paper-raised);
    }

    .collection__grid[data-view-mode="list"] .art-card__frame {
        grid-column: 1;
        grid-row: 1;
    }

    .collection__grid[data-view-mode="list"] .art-card__frame--ratio {
        aspect-ratio: 1 / 1;
        padding: 0;
        border-radius: 12px;
    }

    .collection__grid[data-view-mode="list"] .art-card__frame--ratio img {
        width: 100%;
        height: 100%;
        max-width: none;
        max-height: none;
        object-fit: cover;
        box-shadow: none;
    }

    .collection__grid[data-view-mode="list"] .art-card__frame--ratio picture {
        display: block;
        width: 100%;
        height: 100%;
    }

    .collection__grid[data-view-mode="list"] .art-card__body {
        grid-column: 2;
        grid-row: 1;
        padding-top: 0;
    }

    .collection__grid[data-view-mode="list"] .art-card__title {
        margin: 0 0 0.15rem;
        font-size: 1.05rem;
        line-height: 1.25;
        /* Two lines, then ellipsis — long titles must not set the row height. */
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        overflow: hidden;
    }

    .collection__grid[data-view-mode="list"] .art-card__meta {
        margin: 0;
        font-size: 0.75rem;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }

    .collection__grid[data-view-mode="list"] .art-card__price {
        margin-top: 0.4rem;
        font-size: 0.9375rem;
        font-weight: 600;
    }

    .collection__grid[data-view-mode="list"] .art-card__price .from {
        font-weight: 400;
        font-size: 0.5625rem;
        letter-spacing: 0.12em;
    }

    /* Top right of the row, as a circle. */
    .collection__grid[data-view-mode="list"] .save-btn {
        grid-column: 3;
        grid-row: 1;
        align-self: start;
        justify-self: end;
        width: 30px;
        height: 30px;
        margin-right: -0.15rem;
        border: 0;
        border-radius: 50%;
        background: transparent;
        color: var(--ink-soft);
    }

    .collection__grid[data-view-mode="list"] .save-btn.is-saved {
        background: transparent;
        color: var(--danger);
    }

    /*
     * The thumbnail is 84px and "Original — 1 only" is wider than that at any
     * legible size, so here the pill wraps instead of truncating: "ORIGINA…"
     * tells a visitor nothing, and the scarcity of a one-off original is the
     * thing worth saying. SOLD and FEATURED still fit on one line.
     *
     * Prefixed with .page-shop to outrank the grid-view pill rule further
     * down the file — the two have equal specificity, and that one is later.
     */
    .page-shop .collection__grid[data-view-mode="list"] .art-status {
        top: 0.3rem;
        left: 0.3rem;
        max-width: calc(100% - 0.6rem);
        padding: 0.2rem 0.4rem;
        font-size: 0.4375rem;
        letter-spacing: 0.06em;
        line-height: 1.25;
        text-align: center;
        white-space: normal;
        text-overflow: clip;
    }

    .applied {
        padding-block: 0.9rem;
    }

    .applied__clear {
        margin-left: 0;
    }
}

/* ===========================================================================
   THE ARTIST
   ---------------------------------------------------------------------------
   Stands where the featured grid used to, and carries the studio rather than
   the stock. Laid out as a gallery datasheet: a portrait, the name, what they
   work in, and three figures counted off the catalogue — never typed in.
   Every part is conditional, so an unfilled setting removes its row instead of
   leaving a labelled blank.
   =========================================================================== */
.artist__inner {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
    gap: clamp(2rem, 5vw, 5rem);
    align-items: center;
}

/* With no portrait set the panel is a single grid child, and auto-fit hands it
   the whole shell — a stat row 1300px wide reading three numbers. Capped so it
   holds a column's measure either way. */
.artist__body {
    max-width: 46rem;
}

.artist__portrait {
    position: relative;
    margin: 0;
    aspect-ratio: 4 / 5;
    overflow: hidden;
    border-radius: var(--radius-art);
    background: var(--paper-sunken);
    box-shadow: var(--shadow-frame);
}

.artist__portrait img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* The tag over the corner of the photograph. */
.artist__badge {
    position: absolute;
    top: 1rem;
    left: 1rem;
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.45rem 0.9rem;
    border-radius: 999px;
    background: rgba(var(--hero-scrim), 0.62);
    backdrop-filter: blur(8px) saturate(150%);
    -webkit-backdrop-filter: blur(8px) saturate(150%);
    font-size: 0.62rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink);
}

.artist__badge::before,
.artist__role::before {
    content: '';
    width: 5px;
    height: 5px;
    flex: none;
    border-radius: 50%;
    background: var(--accent);
}

/* A rule runs into the label rather than sitting above it. */
.artist__eyebrow {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    margin: 0 0 1rem;
    font-size: 0.68rem;
    letter-spacing: 0.24em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

.artist__eyebrow::before {
    content: '';
    width: 2.2rem;
    height: 1px;
    background: var(--rule-strong);
}

.artist__name {
    margin: 0 0 1.2rem;
    font-size: clamp(2.4rem, 4.6vw, 4rem);
    font-style: italic;
    line-height: 1.02;
    letter-spacing: -0.01em;
}

.artist__role {
    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    margin: 0 0 1.75rem;
    padding: 0.5rem 1.05rem;
    border: 1px solid var(--rule-strong);
    border-radius: 999px;
    font-size: 0.66rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

.artist__lede {
    max-width: 34rem;
    font-family: var(--font-display);
    font-size: clamp(1.05rem, 1.5vw, 1.3rem);
    line-height: 1.75;
    color: var(--ink-soft);
}

.artist__lede p {
    margin: 0 0 1rem;
}

.artist__lede p:last-child {
    margin-bottom: 0;
}

/* Counted, not claimed — set between hairlines like a catalogue entry. */
.artist__stats {
    /* One row always. Wrapping put the third figure on its own line still
       wearing the divider that separated it from the second, which read as an
       indent rather than a column. */
    display: grid;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    margin: 2.25rem 0 0;
    padding: 1.6rem 0 0;
    list-style: none;
}

.artist__stat {
    display: grid;
    gap: 0.4rem;
    padding-right: clamp(1.25rem, 3vw, 2.5rem);
}

.artist__stat + .artist__stat {
    padding-left: clamp(1.25rem, 3vw, 2.5rem);
    border-left: 1px solid var(--rule);
}

.artist__stat-value {
    font-family: var(--font-display);
    font-size: clamp(1.7rem, 2.6vw, 2.3rem);
    font-style: italic;
    line-height: 1;
    color: var(--ink);
}

.artist__stat-label {
    font-size: 0.66rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink-faint);
}

/* The two bands the About page adds under the datasheet: the studio photos and
   the films. Both are auto-fit grids, so one photo is not stretched across the
   shell and twelve do not become postage stamps. */
.artist__gallery {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr));
    gap: 0.75rem;
}

.artist__gallery figure {
    margin: 0;
}

.artist__gallery img {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: var(--radius-art);
}

.artist__film {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
    gap: 1rem;
}

.artist__film > div {
    aspect-ratio: 16 / 9;
    overflow: hidden;
    border-radius: var(--radius-art);
    /* An ink surface, not --paper: a video letterboxes, and the bars should be
       the dark of a cinema in either theme rather than the page's own ground. */
    background: var(--ink-surface);
}

.artist__film video,
.artist__film iframe {
    width: 100%;
    height: 100%;
    border: 0;
    object-fit: cover;
}

/* What a digital file is and is not. Stated plainly, before the price, and
   never styled as a warning — it is a fact about the product, not an alarm. */
.digital-notice {
    margin: 0 0 1.25rem;
    padding: 0.85rem 1rem;
    border-left: 2px solid var(--accent);
    background: var(--paper-sunken);
    font-size: 0.85rem;
    line-height: 1.6;
    color: var(--ink-soft);
}

/* ===========================================================================
   INTRODUCTION — index.php
   Three chapters and three buttons. Deliberately not scroll-driven: the whole
   sequence fits one screen and advances only when somebody asks it to.
   =========================================================================== */
.intro {
    /* svh, not vh: on a phone the browser chrome collapses as you scroll, and
       vh is measured against the expanded state — so the controls sat under the
       toolbar exactly when someone reached for them. */
    min-height: calc(100svh - var(--header-h));
    display: grid;
    place-items: center;
    padding-block: clamp(2rem, 6vw, 5rem);
}

.intro__inner {
    display: grid;
    gap: clamp(1.75rem, 4vw, 2.75rem);
    justify-items: center;
    text-align: center;
    max-width: 54rem;
}

.intro__chapters {
    list-style: none;
    margin: 0;
    padding: 0;
    width: 100%;
}

.intro__chapter {
    display: grid;
    gap: 1rem;
    justify-items: center;
}

.intro__title {
    margin: 0;
    font-family: var(--font-display);
    font-size: clamp(2.25rem, 7vw, 4.25rem);
    line-height: 1.05;
    font-weight: 300;
}

/* The heading takes focus on every step so a screen reader and a keyboard land
   on the new content — but it is focus moved by us, not by a click, so it must
   not draw a ring. :focus-visible still does, for real keyboard focus. */
.intro__title:focus {
    outline: none;
}

.intro__body {
    margin: 0;
    max-width: 46ch;
    font-size: 1.05rem;
    line-height: 1.75;
    color: var(--ink-soft);
}

.intro__ways {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    justify-content: center;
    margin-top: 0.5rem;
}

.intro__film {
    width: 100%;
    max-width: 40rem;
    margin-top: 0.5rem;
    border-radius: var(--radius-art);
    overflow: hidden;
    background: var(--ink-surface);
}

.intro__film video {
    width: 100%;
    height: auto;
    display: block;
}

.intro__controls {
    display: flex;
    align-items: center;
    gap: clamp(1rem, 4vw, 2rem);
}

/* 44px minimum in both directions — the control has to be reachable one-handed
   on a phone, which is the only way most people will meet this page. */
.intro__btn {
    min-height: 44px;
    min-width: 44px;
}

.intro__btn[disabled] {
    opacity: 0.35;
    pointer-events: none;
}

.intro__progress {
    display: flex;
    gap: 0.5rem;
    list-style: none;
    margin: 0;
    padding: 0;
}

.intro__progress li {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--rule-strong);
    transition: background var(--quick) var(--ease);
}

.intro__progress li.is-on {
    background: var(--accent);
}

.intro__skip {
    font-size: 0.78rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--ink-faint);
    text-decoration: underline;
    text-underline-offset: 4px;
    padding: 0.5rem;
}

.intro__skip:hover {
    color: var(--ink);
}

/*
 * With JavaScript off there is nothing to drive the sequence, so every chapter
 * is shown at once and the controls — which would do nothing — are removed.
 * Skip is a real anchor and keeps working.
 */
.no-js .intro {
    min-height: 0;
}

.no-js .intro__chapter[hidden] {
    display: grid;
}

.no-js .intro__chapter + .intro__chapter {
    margin-top: clamp(2.5rem, 6vw, 4rem);
}

.no-js .intro__controls {
    display: none;
}

@media (max-width: 560px) {
    .intro__controls {
        width: 100%;
        justify-content: space-between;
    }

    .intro__ways {
        flex-direction: column;
        align-self: stretch;
    }
}

/* A phone held sideways is about 375px tall: the chapter has to be allowed to
   scroll rather than be centred in a box it cannot fit. */
@media (max-height: 520px) {
    .intro {
        min-height: 0;
        place-items: start center;
    }
}

/* ===========================================================================
   FEATURED PIECE — one painting, given the room
   =========================================================================== */
.feature__inner {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
    gap: clamp(2rem, 5vw, 4rem);
    align-items: center;
}

.feature__frame {
    margin: 0;
}

/* No aspect-ratio and no object-fit. The painting keeps its own proportions —
   cropping the work to a tidy rectangle is the one thing this band must not do. */
.feature__frame img {
    width: 100%;
    height: auto;
    border-radius: var(--radius-art);
    box-shadow: var(--shadow-frame);
}

.feature__body {
    max-width: 34rem;
}

.feature__title {
    margin: 0.5rem 0 0.35rem;
    font-family: var(--font-display);
    font-size: clamp(1.9rem, 4vw, 2.75rem);
    line-height: 1.1;
    font-weight: 300;
}

.feature__meta {
    margin: 0 0 1rem;
    font-size: 0.85rem;
    color: var(--ink-faint);
}

.feature__lede {
    margin: 0 0 1.75rem;
    color: var(--ink-soft);
    line-height: 1.75;
}

/* ===========================================================================
   CERTIFICATE VERIFICATION — verify-coa.php
   ---------------------------------------------------------------------------
   A page reached by scanning a code on a piece of paper, usually by someone
   who is not a customer and has never seen this site. It gets one job: say
   whether the certificate is real, in language a stranger can act on.
   =========================================================================== */

.verify__headline {
    font-size: clamp(2rem, 4.5vw, 3rem);
    margin-bottom: 0.5rem;
}

.verify__body,
.verify__note {
    max-width: 60ch;
    margin-top: 1.25rem;
}

.verify__contact {
    margin-top: 2.5rem;
}

.verify__contact a {
    border-bottom: 1px solid var(--rule-strong);
}

.verify-card {
    position: relative;
    margin-top: clamp(2rem, 4vw, 3rem);
    padding: clamp(1.75rem, 4vw, 2.75rem);
    background: var(--paper-raised);
    border: 1px solid var(--rule);
    border-top: 3px solid var(--success);
}

.verify-card--revoked {
    border-top-color: var(--danger);
}

.verify-badge {
    display: inline-block;
    padding: 0.3rem 0.7rem;
    margin-bottom: 1.25rem;
    font-size: 0.625rem;
    font-weight: 500;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--paper);
    background: var(--success);
}

.verify-badge--revoked {
    background: var(--danger);
}

.verify-card__title {
    font-size: clamp(1.5rem, 3vw, 2.1rem);
    margin: 0 0 0.35rem;
}

.verify-card__meta {
    color: var(--ink-faint);
    font-size: 0.875rem;
    margin: 0;
}

.verify-card__body {
    max-width: 58ch;
    margin: 1.5rem 0 0;
    color: var(--ink-soft);
}

.verify-facts {
    margin: clamp(1.5rem, 3vw, 2.25rem) 0 0;
}

.verify-facts__row {
    display: grid;
    grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
    gap: 1rem;
    padding-block: 0.8rem;
    border-bottom: 1px solid var(--rule);
}

.verify-facts dt {
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink-faint);
    align-self: center;
}

.verify-facts dd {
    margin: 0;
    font-family: var(--font-display);
    font-size: 1.05rem;
    color: var(--ink);
}

.verify-form {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-top: clamp(1.75rem, 3vw, 2.5rem);
}

.verify-form input {
    flex: 1 1 16rem;
    min-width: 0;
    border: 1px solid var(--rule-strong);
    background: var(--paper-raised);
    padding: 0.85rem 1rem;
    font-family: var(--font-body);
    font-size: 1rem;
    letter-spacing: 0.04em;
    color: var(--ink);
}

.verify-form input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-wash);
}

@media (max-width: 600px) {
    .verify-facts__row {
        grid-template-columns: 1fr;
        gap: 0.25rem;
    }
}

/* ===========================================================================
   THE COLLECTION ON A PHONE — the gallery layout
   ---------------------------------------------------------------------------
   A phone gets a different reading order from the desktop. There, the title
   sits over a sidebar of facets and the toolbar is a sticky utility strip;
   here the page reads as a single column — title, search, controls, lenses,
   work — with the artwork carried edge to edge in two rounded tiles.

   None of it is a second copy of the markup. <main> becomes a flex column at
   this width and each band is given an order, so the same elements simply
   stack in a different sequence. Everything above 700px is untouched.
   =========================================================================== */
@media (max-width: 700px) {

    body.page-shop #main {
        display: flex;
        flex-direction: column;
    }

    /* The scrim, the tab bar and the page script are all either hidden or
       fixed, so they keep the default order 0 without appearing above the
       title. Only the five real bands are placed. */
    .page-shop .collection-head { order: 1; }
    .page-shop .toolbar         { order: 2; }
    .page-shop .lens            { order: 3; }
    .page-shop .applied-shell   { order: 4; }
    .page-shop .collection      { order: 5; }

    /* ------------------------------------------------------------- header */
    /*
     * The gallery header carries two things and no more: the menu, boxed, on
     * the left, and the cart on the right. The wordmark stands down — the
     * page's own masthead is the very next thing on screen, so the brand is
     * said twice in the first 120px otherwise, and Home is already under the
     * thumb in the tab bar.
     *
     * .header-actions is dissolved with display:contents so the burger and
     * the cart become items of the header row itself and can be sent to
     * opposite ends of it. Nothing moves in the markup, so every other page
     * keeps its wordmark and its icon cluster exactly as they were.
     */
    .page-shop .site-header .wordmark {
        display: none;
    }

    .page-shop .site-header .header-actions {
        display: contents;
    }

    .page-shop .site-header .nav-toggle {
        order: -1;
        width: 38px;
        height: 38px;
        border: 1px solid var(--rule-strong);
        border-radius: 11px;
    }

    .page-shop .site-header .nav-toggle:hover {
        border-color: var(--ink-faint);
    }

    .page-shop .site-header .icon-btn--cart {
        order: 1;
        /* The badge is pinned to the button's corner, which is now the very
           edge of the screen. Pull it back inside the gutter. */
        margin-right: -0.35rem;
    }

    /* --------------------------------------------------------------- head */
    /* One line of voice and nothing else: the breadcrumb and the lede are
       both repeating what the header and the title already say, and on a
       phone they cost the first screenful of artwork. */
    .page-shop .collection-head {
        padding-block: 0.35rem 1.15rem;
    }

    .page-shop .crumbs,
    .page-shop .collection-head__lede {
        display: none;
    }

    .page-shop .collection-head__title {
        font-size: clamp(2.85rem, 14vw, 3.9rem);
        line-height: 0.96;
        letter-spacing: -0.015em;
        text-wrap: balance;
    }

    /* "Art Pieces" reads as one line here, as it does on the desktop. The words
       are spans rather than a <br> — the markup joins them with a space, so
       leaving them inline is all that is needed, and the breadcrumb, the <title>
       and the schema still read the page's name as one flat string. Only the
       default heading is split at all; a filtered or search heading is already
       a single string and wraps on its own. */
    .page-shop .collection-head__line {
        display: inline;
    }

    /* ------------------------------------------------------------ search */
    /* Not sticky here. A pill pinned under the header takes a fifth of a
       phone screen for a field nobody is using while they scroll. */
    .page-shop .toolbar {
        position: static;
        background: transparent;
        border-bottom: 0;
    }

    .page-shop .toolbar__inner {
        display: block;
        min-height: 0;
        padding-block: 0;
    }

    .page-shop .toolbar__search {
        order: 0;
        width: 100%;
        max-width: none;
        gap: 0.8rem;
        padding-inline: 1.15rem;
        padding-block: 0;
        border: 1px solid var(--rule);
        border-radius: 999px;
        background: var(--paper-raised);
    }

    .page-shop .toolbar__search-input {
        /* Safari zooms the page for anything under 16px and never zooms back. */
        font-size: 16px;
        padding-block: 0.95rem;
    }

    /* ----------------------------------------------------------- controls */
    /* Filters, sort and the layout switch on one line under the field —
       where the reference puts its section rule. */
    /*
     * The toolbar row becomes a section header: the name of what is on
     * screen, and one way into the filters. The sort select and the layout
     * switch move into the filter sheet — a row of three controls above the
     * work is what the design replaced with a title.
     */
    .page-shop .toolbar__tools {
        width: 100%;
        gap: 0.6rem;
        align-items: baseline;
        justify-content: space-between;
        padding-top: 1.1rem;
    }

    /* The reference sets this row as a UI heading — bold sans, tight — rather
       than the serif the rest of the page titles use, with a quiet control
       opposite it instead of a bordered pill. */
    .page-shop .toolbar__section {
        display: block;
        font-family: var(--font-body);
        font-weight: 600;
        font-size: 1.0625rem;
        letter-spacing: -0.01em;
        line-height: 1.1;
        color: var(--ink);
    }

    .page-shop .toolbar__sort,
    .page-shop .view-switch {
        display: none;
    }

    /* Its own control, so the phone can still sort. */
    .page-shop .facet--sort {
        display: block;
    }

    .facet--sort .facet__sort-select {
        width: 100%;
        margin-top: 0.5rem;
        padding: 0.7rem 0.9rem;
        border: 1px solid var(--rule);
        border-radius: 10px;
        background: var(--paper-raised);
        color: var(--ink);
        font-family: var(--font-body);
        font-size: 0.9375rem;
    }

    /* The design has no count line above the work. */
    .page-shop .collection__meta {
        display: none;
    }

    /*
     * And no badges across the paintings. Sold, featured and last-one-left
     * are all on the artwork page; on a 165px tile they cover a third of the
     * picture, which is the thing the visitor came to look at.
     */
    .page-shop .collection__grid[data-view-mode="grid"] .art-status {
        display: none;
    }

    /* One caption line, as the design has: the title, and the price above it.
       Medium and size are a tap away and are on every card in list view. */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__meta {
        display: none;
    }


    .page-shop .toolbar__btn {
        border: 0;
        background: none;
        color: var(--ink-soft);
        letter-spacing: 0.06em;
        text-transform: none;
        padding: 0.6rem 0.95rem;
        font-size: 0.65rem;
    }

    .page-shop .toolbar__select {
        font-size: 0.8125rem;
    }

    .page-shop .view-switch__btn {
        width: 34px;
        height: 34px;
    }

    /* -------------------------------------------------------------- lenses */
    /* The category strip: sentence case, no band rule, scrolling off the
       right gutter so it is obvious there is more than fits. */
    .page-shop .lens {
        margin-top: 1.4rem;
        border-bottom: 0;
    }

    .page-shop .lens__track {
        gap: 1.5rem;
    }

    .page-shop .lens__tab {
        padding-block: 0.25rem 0.45rem;
        font-size: 0.8125rem;
        font-weight: 500;
        letter-spacing: 0.01em;
        text-transform: none;
    }

    .page-shop .lens__tab.is-active {
        font-weight: 600;
    }

    .page-shop .lens__tab::after {
        bottom: 0;
    }

    /* ------------------------------------------------------------- results */
    .page-shop .collection {
        padding-top: 1.35rem;
    }

    .page-shop .applied {
        border-bottom: 0;
        padding-block: 1rem 0;
    }

    .page-shop .collection__meta {
        padding-bottom: 0;
        margin-bottom: 1.15rem;
        border-bottom: 0;
        font-size: 0.65rem;
        letter-spacing: 0.16em;
    }

    /* --------------------------------------------------------- the puzzle */
    /*
     * A justified mosaic: every row is filled edge to edge, tiles come out at
     * different widths AND different heights, and not one of them is cropped.
     *
     * The arithmetic is the whole trick, and it is done by flexbox rather than
     * by script. Give each tile a basis and a grow factor both proportional to
     * its own aspect ratio, and flex settles every tile in a row at
     *
     *     width = ratio x k          (k being whatever fits the row)
     *
     * so its height — width / ratio — comes out as k for every tile in that
     * row, whatever shape it is. Equal heights, straight rows, true
     * proportions. Rows differ in height because their contents differ, which
     * is what makes it read as a puzzle rather than a table.
     *
     * This is what the earlier attempts each got half of: three even columns
     * kept the proportions but could not vary the width, and the span grid
     * varied the width but had to crop to fill its cells. --ar comes from the
     * card partial, off the file's real pixel dimensions.
     */
    .page-shop .collection__grid[data-view-mode="grid"] .art-grid {
        display: flex;
        flex-wrap: wrap;
        gap: 6px;
    }

    /* ------------------------------------------------------------- a tile */
    /* The frame carries the piece's own ratio, which is what makes the row
       arithmetic above come out level. Falls back to a square if a piece has
       no recorded dimensions. No mat, no shadow — just the picture. */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__frame--ratio {
        display: block;
        aspect-ratio: var(--ar, 1);
        height: auto;
        padding: 0;
        border-radius: 14px;
        background: var(--paper-sunken);
    }

    .page-shop .collection__grid[data-view-mode="grid"] .art-card__frame--ratio picture {
        display: block;
        width: 100%;
        height: 100%;
    }

    .page-shop .collection__grid[data-view-mode="grid"] .art-card__frame--ratio img {
        width: 100%;
        height: 100%;
        max-width: none;
        max-height: none;
        /* The box is built from this file's own ratio, so cover and contain
           agree and nothing is actually cut. cover only in case a thumbnail is
           ever regenerated at a different shape than the row was solved for —
           a hairline crop then, rather than a letterbox in a flush mosaic. */
        object-fit: cover;
        box-shadow: none;
    }

    /* The status pill is the one thing that has to sit ON the artwork, and
       "Original — 1 only" is wider than a 165px tile. Truncate rather than
       let it hang out over the gutter.

       It also loses the mat it used to sit on, so the tint alone no longer
       separates it from a busy painting. The scrim carries the contrast and
       the status keeps its colour in the text. */
    .page-shop .collection__grid .art-status {
        top: 0.5rem;
        left: 0.5rem;
        max-width: calc(100% - 1rem);
        padding: 0.22rem 0.5rem;
        font-size: 0.5rem;
        letter-spacing: 0.09em;
        overflow: hidden;
        text-overflow: ellipsis;
        background-color: rgba(10, 10, 12, 0.7);
    }

    /* ------------------------------------------------------------ caption */
    /*
     * The caption is re-ordered into the shape a phone reads fastest: price
     * and the save control on one line, then what the piece is.
     *
     * .art-card__body is dissolved with display:contents so its three
     * children land on the card's own grid next to the save button — which
     * has to stay a direct child of .art-card, because the desktop list view
     * places it there. Nothing moves in the markup.
     */
    /*
     * Both the basis and the grow factor are the piece's ratio — that pairing
     * is what makes a row's heights come out equal. 7.5rem is the row's
     * nominal height before flex justifies it; a wide piece therefore claims
     * proportionally more of the row than a tall one, exactly as it should.
     */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card {
        position: relative;
        display: block;
        flex: var(--ar, 1) 1 calc(var(--ar, 1) * 6.5rem);
        min-width: 0;
        margin-bottom: 0;
    }

    /*
     * The caption comes off the grid and onto the picture. A cell is a fixed
     * height now, so a caption stacked underneath would either overflow it or
     * steal height from the artwork; laid over the foot of the tile on a
     * scrim it costs nothing and the mosaic stays flush.
     */
    /* Below the tile on a phone too. The note above argued the caption had to
       ride on a scrim here because a mosaic tile has no spare height — but the
       height it was protecting was the artwork's, and it paid for it by
       covering the artwork. A row that is one caption taller is the cheaper
       cost of the two. */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__body {
        display: block;
        position: static;
        padding: 0.5rem 0 0;
        background: none;
        border-radius: 0;
        pointer-events: auto;
    }

    /* Still never wraps — a price broken across two lines reads as damage —
       but on the page's ink now rather than on a scrim that is gone. */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__price {
        margin: 0.2rem 0 0;
        font-size: 0.6875rem;
        font-weight: 600;
        letter-spacing: -0.01em;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        color: inherit;
    }

    /*
     * "Prints from" is gone from the tile. It turned one price into two
     * lines of small caps, and the design has a single figure per card. The
     * artwork page still says which price is which.
     */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__price .from {
        display: none;
    }

    /* "or prints from …" is a third line on a two-line caption. The headline
       price already carries it; the artwork page has the rest. */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__price br,
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__price br ~ .from {
        display: none;
    }

    /* Back into the tile's own corner: there is no caption row under the
       picture to sit on any more. Its own dark disc, because it lands on
       whatever the painting happens to be. */
    .page-shop .collection__grid[data-view-mode="grid"] .save-btn {
        position: absolute;
        top: 0.35rem;
        right: 0.35rem;
        width: 26px;
        height: 26px;
        margin: 0;
        border-radius: 50%;
        background: rgba(10, 10, 12, 0.55);
        backdrop-filter: blur(6px);
        -webkit-backdrop-filter: blur(6px);
        color: var(--ink-surface-strong);
        opacity: 1;
        transform: none;
    }

    .page-shop .collection__grid[data-view-mode="grid"] .save-btn.is-saved,
    .page-shop .collection__grid[data-view-mode="grid"] .save-btn:hover {
        background: transparent;
        color: var(--danger);
    }

    /*
     * The title comes off the tile. On a one-column cell it is four or five
     * legible characters before the ellipsis, which names nothing, and laid
     * over a painting it is another thing between the reader and the work.
     * The price stays because it is the decision being made; the title is on
     * the artwork page, one tap away, in full.
     */
    .page-shop .collection__grid[data-view-mode="grid"] .art-card__title {
        display: none;
    }
}


/* The subgrid pass that used to sit here lined every caption up across the two
   columns. It needed the cards to be cells of one grid, and it needed them to
   be the same height to be worth doing — neither is true now that each tile is
   the shape of its own painting and the columns stagger by design. */

/* ===========================================================================
   THE ARTWORK PAGE ON A PHONE
   ---------------------------------------------------------------------------
   The desktop page is two columns: a sticky viewer beside a long buy panel.
   A phone gets the gallery reading instead — an arched plate, the title, what
   the piece is, its measurements as tiles, and the price and the buy button
   pinned under the thumb where they can be reached without scrolling back.

   Same markup throughout. The one thing that is genuinely new is the buy bar,
   which cannot be a moved copy of the panel's button because a fixed element
   has to be a sibling of nothing in particular.
   =========================================================================== */

/* The back control is the phone's wayfinding; the crumb trail is the
   desktop's. Only one of them is ever on screen. */
.back-btn,
.cart-btn {
    display: none;
}

.save-btn--inline {
    display: none;
}

/* Off-canvas by default so it cannot cover a desktop page. */
.buybar {
    display: none;
}

@media (max-width: 700px) {

    /* ------------------------------------------------------------- header */
    /*
     * No site header on this screen. The piece is the whole point of the
     * page and it starts at the top of the glass; the back control below is
     * the only chrome. The header is hidden rather than not rendered, so the
     * cart badge it carries still updates and the markup stays common to
     * every page.
     *
     * The row carries its own two controls instead: back on the left, cart on
     * the right. There is still no menu from here — the back arrow returns to
     * Art Pieces, which has one.
     */
    .page-artwork .site-header {
        display: none;
    }

    /* ---------------------------------------------------------- back row */
    /* First thing under the status bar now, so it owns the inset. */
    .page-artwork .artwork-top {
        padding-block: calc(0.9rem + env(safe-area-inset-top)) 0.75rem;
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
    }

    .page-artwork .artwork-top .eyebrow {
        display: none;
    }

    .back-btn,
    .cart-btn {
        display: grid;
        place-items: center;
        width: 38px;
        height: 38px;
        border: 1px solid var(--rule-strong);
        border-radius: 11px;
        color: var(--ink);
    }

    /* The badge hangs off the corner rather than sitting inside it: this button
       is 38px against the header's 44px, and the count would crowd the glyph. */
    .cart-btn {
        position: relative;
    }

    .cart-btn .badge-count {
        top: -6px;
        right: -6px;
    }

    /* ------------------------------------------------------------- plate */
    /*
     * A plain plate at the painting's own proportions. Two things used to crop
     * it and both are gone: the arch, whose top corners carried a horizontal
     * radius of half the width and a vertical radius of 37.5% to draw an exact
     * semicircle, and the 3:4 box underneath it. That box was the worse of the
     * two — a landscape canvas forced into a portrait frame loses its sides,
     * not a sliver.
     *
     * The ratio is now the lead image's own on every width rather than
     * something the phone imposes, so nothing is cropped and the plate is still
     * one fixed box — which is what lets a clip and a painting sit in the same
     * frame instead of each setting its own height in a horizontal track. It is
     * reserved before the picture arrives, so the page below does not jump.
     */
    .page-artwork .artwork-layout {
        gap: 1.5rem;
    }

    /* A phone is portrait, so a tall frame costs the whole screen. Capped
       nearer the fold than the desktop's; the ratio and the way the cap is
       applied are unchanged. */
    .page-artwork .viewer__stage {
        --stage-cap: max(16rem, calc(100svh - var(--header-h) - 14rem));
    }

    /* The full-screen control sat in a square corner that is now curved. */
    .page-artwork .viewer__tools {
        bottom: 0.9rem;
        right: 0.9rem;
    }

    /* ------------------------------------------------------------- title */
    .page-artwork .buy-panel__head {
        display: flex;
        align-items: flex-start;
        justify-content: space-between;
        gap: 1rem;
    }

    .page-artwork .buy-panel__title {
        font-size: clamp(2.1rem, 9.5vw, 3rem);
        line-height: 1.02;
        margin-bottom: 0.5rem;
    }

    .page-artwork .save-btn--inline {
        display: grid;
        position: static;
        flex: none;
        width: 34px;
        height: 34px;
        margin-top: 0.5rem;
        background: transparent;
        color: var(--ink);
        opacity: 1;
        transform: none;
    }

    .page-artwork .save-btn--inline.is-saved {
        background: transparent;
        color: var(--ink);
    }

    /* The buy panel keeps its own save control for the desktop; two hearts
       on one phone screen is one too many. */
    .page-artwork .qty-row .save-btn {
        display: none;
    }

    /* -------------------------------------------------------- spec tiles */
    /* The specification stops being a table of rows and becomes a strip of
       tiles that scrolls, so the measurements stay one glance rather than
       six lines of the page. */
    .page-artwork .spec-list {
        display: flex;
        gap: 0.6rem;
        margin: 0 0 1.75rem;
        padding-bottom: 0.25rem;
        border-top: 0;
        overflow-x: auto;
        scrollbar-width: none;
        /* Runs off the gutter, so it is obvious the strip continues. */
        margin-inline: calc(var(--gutter) * -1);
        padding-inline: var(--gutter);
    }

    .page-artwork .spec-list::-webkit-scrollbar {
        display: none;
    }

    .page-artwork .spec-list li {
        display: flex;
        flex-direction: column;
        flex: 0 0 auto;
        gap: 0.3rem;
        min-width: 5.5rem;
        max-width: 11rem;
        padding: 0.7rem 0.85rem;
        border: 1px solid var(--rule);
        border-bottom: 1px solid var(--rule);
        border-radius: 14px;
        background: var(--paper-raised);
    }

    .page-artwork .spec-list .label {
        font-size: 0.5625rem;
        font-weight: 500;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        color: var(--ink-faint);
    }

    .page-artwork .spec-list .value {
        text-align: left;
        font-size: 0.8125rem;
        line-height: 1.35;
        color: var(--ink);
    }

    /* Inside a tile the secondary measurements are a third line of noise. */
    .page-artwork .spec-list .value br,
    .page-artwork .spec-list .value .muted {
        display: none;
    }

    /* ----------------------------------------------------------- buy bar */
    /*
     * The panel keeps its own price and button — they are the desktop's, and
     * on a phone they are what the bar mirrors as you scroll past them.
     */
    .buybar {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        /* Above the page, below the burger menu (90), search (100), the
           filter sheet (110/120) and the lightbox (200). */
        z-index: 80;
        display: block;
        /* Follows the theme — .buybar__price below is var(--ink). */
        background: rgba(var(--hero-scrim), 0.92);
        backdrop-filter: saturate(180%) blur(14px);
        -webkit-backdrop-filter: saturate(180%) blur(14px);
        border-top: 1px solid var(--rule);
    }

    .buybar__inner {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 1rem;
        padding-block: 0.7rem;
        padding-bottom: calc(0.7rem + env(safe-area-inset-bottom));
    }

    .buybar__price {
        font-family: var(--font-display);
        font-size: 1.75rem;
        line-height: 1;
        color: var(--ink);
        white-space: nowrap;
    }

    .buybar__cta {
        flex: 0 1 12rem;
        border-radius: 999px;
        padding-block: 0.95rem;
        text-align: center;
    }

    /* The page has to end above the bar rather than behind it. */
    body.has-buybar {
        padding-bottom: calc(72px + env(safe-area-inset-bottom));
    }
}


/* ---------------------------------------------------------------------------
   Testimonials as a carousel on a phone
   ---------------------------------------------------------------------------
   Three quote cards stacked is most of a phone screen each and roughly a
   screen and a half of scrolling to read opinions nobody asked for yet. Side
   by side they are one gesture, and the peek of the next card is what says
   there is more than one.

   Same idiom as the home rails: run off the gutter, snap, and give the snap
   port the gutter back as scroll-padding — a snap port measures from the
   padding edge, so without it the first card lands flush against the screen.
   --------------------------------------------------------------------------- */
@media (max-width: 700px) {
    .quote-grid {
        display: flex;
        gap: 0.85rem;
        overflow-x: auto;
        scrollbar-width: none;
        -webkit-overflow-scrolling: touch;
        scroll-snap-type: x mandatory;
        scroll-padding-inline: var(--gutter);
        margin-inline: calc(var(--gutter) * -1);
        padding-inline: var(--gutter);
        padding-bottom: 0.25rem;
    }

    .quote-grid::-webkit-scrollbar {
        display: none;
    }

    .quote {
        /* Just under full width, so the next card shows at the edge. */
        flex: 0 0 82%;
        scroll-snap-align: start;
        border-radius: 16px;
    }

    /* The cards are laid out left to right but revealed on vertical scroll.
       One that is parked off to the right is still "below the fold" as far as
       the observer is concerned, so it would sit at opacity 0 until the
       carousel is dragged — which looks like a card that failed to load. */
    .quote-grid .quote.reveal {
        opacity: 1;
        transform: none;
    }
}

@media print {

    .site-header,
    .site-footer,
    .btn,
    .flash-stack {
        display: none !important;
    }

    body {
        background: #fff;
    }
}
/* ------------------------------------------------------ commission brief */
/*
   A twenty-page paper brief on one page. The whole job here is to stop it
   reading as a wall: each numbered section is a card with air around it, so a
   visitor can see where they are and how much is left, and every question keeps
   the same .field furniture as the rest of the site's forms rather than growing
   a second look of its own.
*/
.brief-section {
    padding: clamp(1.25rem, 2.5vw, 2rem);
    margin-bottom: clamp(1.25rem, 2.5vw, 2rem);
    border: 1px solid var(--rule);
    border-radius: var(--radius, 4px);
}

.brief-section__title {
    font-size: 1.05rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    margin: 0;
}

.brief-section__lede {
    margin: 0.5rem 0 1.25rem;
    max-width: 68ch;
}

/* Two columns for the short answers, one for anything that needs room. The
   grid is what keeps a name and an email on the same line without either
   question having to know it has a neighbour. */
.field-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0 1rem;
}

.field-grid > .field--full,
.field-grid > .field--group,
.field-grid > fieldset {
    grid-column: 1 / -1;
}

@media (max-width: 700px) {
    .field-grid {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* A fieldset is not a .field: it carries a legend instead of a label and the
   browser's own border, which is removed here rather than reset globally. */
.field--group {
    border: 0;
    padding: 0;
    margin: 0 0 1.25rem;
    min-width: 0;
}

.field--group legend {
    padding: 0;
    margin-bottom: 0.6rem;
    font-size: 0.8125rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ink-soft);
}

/* Options wrap into as many columns as fit. auto-fill, not auto-fit: a group of
   two answers should stay two narrow columns rather than stretching one answer
   across the whole width and reading as a heading. */
.brief-options {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: 0.5rem 1rem;
}

.brief-group {
    margin: 1.5rem 0;
    padding-top: 1.25rem;
    border-top: 1px solid var(--rule);
}

.brief-group__title {
    font-size: 0.8125rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink-soft);
    margin: 0 0 0.9rem;
}

/* The PDF's 1-7 slider. Radios rather than an <input type=range>, because a
   range with no visible value looks answered when nobody has touched it. */
.brief-scale {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.brief-scale__end {
    font-size: 0.8125rem;
    color: var(--ink-soft);
    min-width: 7ch;
}

.brief-scale__end:last-of-type {
    text-align: right;
}

.brief-scale__step {
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0.2rem;
    font-size: 0.75rem;
    color: var(--ink-soft);
    cursor: pointer;
}

.brief-scale__step input {
    accent-color: var(--accent-deep);
}

/* The commission call to action on a listing or category header. A ghost button
   and one line of explanation: it is an alternative to browsing, not the thing
   the page is for, and a solid button here would outrank the work. */
.collection-head__cta {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    flex-wrap: wrap;
    margin-top: 1.25rem;
}

/* .field label is the site's field caption — uppercase, tracked out, its own
   line. A checkbox's label is none of those things: it is the answer itself,
   sitting beside its box. The fieldset carries .field for its margins, so the
   caption rule reaches these too and has to be undone here rather than by
   dropping the class and re-stating the spacing. */
.brief-options .check,
.brief-scale__step {
    display: flex;
    text-transform: none;
    letter-spacing: normal;
    font-size: 0.875rem;
    color: var(--ink-soft);
    margin-bottom: 0;
}

.brief-scale__step {
    flex-direction: column;
    align-items: center;
    font-size: 0.75rem;
}

.brief-options .check span {
    line-height: 1.35;
}

/* The links to the other briefs. Spaced, because two questions run together
   read as one sentence that answers neither. */
.contact-swap {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1.5rem;
}

/* ------------------------------------------------------- payment handover */
/*
   The screen between checkout and the payment provider, and the one they come
   back to when a payment is declined. It is deliberately quiet and narrow: at
   this moment the customer wants the amount, the provider's name and a way
   onward — anything else on the page is a reason to hesitate.
*/
.handover {
    max-width: 34rem;
    margin-inline: auto;
    padding: clamp(1.5rem, 4vw, 2.5rem);
    border: 1px solid var(--rule);
    text-align: center;
}

.handover__lede {
    font-size: 1.0625rem;
    margin: 0 0 1.25rem;
}

.handover__amount {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    align-items: center;
    margin: 1.5rem 0;
}

.handover__amount strong {
    font-family: var(--font-display, inherit);
    font-size: clamp(1.75rem, 5vw, 2.5rem);
    line-height: 1;
}

.handover__lines {
    list-style: none;
    margin: 0 0 1.75rem;
    padding: 1rem 0 0;
    border-top: 1px solid var(--rule);
    text-align: left;
    font-size: 0.875rem;
}

.handover__lines li {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.35rem 0;
}

.handover__actions {
    display: flex;
    gap: 0.75rem;
    justify-content: center;
    flex-wrap: wrap;
    margin-top: 0.5rem;
}

/* ===========================================================================
   SERIES STACKS
   One stack per subject in the gallery, with its pieces inside it. Replaces the
   flat card grid on the unfiltered listing — see the note above
   Artwork::paginateSeries() for why the gallery groups at all.
   =========================================================================== */

.art-stacks {
    display: grid;
    /* Wide tracks on purpose. A stack is a subject, not a thumbnail, and the
       whole point of the change is that a painting gets room to be looked at. */
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 27rem), 1fr));
    gap: clamp(2rem, 4vw, 3.5rem);
}

.art-stack {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    min-width: 0;
}

.art-stack__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    padding-bottom: 0.55rem;
    border-bottom: 1px solid var(--line, rgba(0, 0, 0, .12));
}

.art-stack__series {
    font-family: var(--font-display);
    font-size: 1.05rem;
    letter-spacing: 0.02em;
    margin: 0;
}

.art-stack__series a {
    text-decoration: none;
    color: inherit;
}

.art-stack__series a:hover {
    text-decoration: underline;
    text-underline-offset: 0.25em;
}

.art-stack__count {
    font-size: 0.75rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    opacity: 0.6;
    white-space: nowrap;
}

/* The arrows are positioned against this, not against the scroller, so they do
   not travel with the content they are scrolling. */
.art-stack__viewport {
    position: relative;
    min-width: 0;
}

.art-stack__track {
    display: flex;
    gap: 1rem;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scroll-behavior: smooth;
    scrollbar-width: none;
    /* The horizontal axis belongs to this track; the vertical one stays with
       the page. Without this a stack swallows an upward flick on a phone and
       the gallery becomes something you get stuck in. */
    touch-action: pan-y pinch-zoom;
    /* Room for the focus ring, which a flush-clipped scroller would cut off. */
    padding: 3px;
    margin: -3px;
}

.art-stack__track::-webkit-scrollbar {
    display: none;
}

.art-stack__slide {
    position: relative;
    flex: 0 0 100%;
    scroll-snap-align: center;
    min-width: 0;
}

/* A stack of one is not a carousel: no track behaviour, no snapping, nothing
   to scroll. It renders as a single piece that happens to share the markup. */
.art-stack--solo .art-stack__track {
    overflow: visible;
    scroll-snap-type: none;
}

.art-stack__frame {
    display: block;
    position: relative;
    overflow: hidden;
    border-radius: var(--radius-art, 2px);
    background: var(--surface-2, rgba(0, 0, 0, .04));
    /* One shape for every slide in a stack, so flicking through does not make
       the caption below jump up and down the page. contain, not cover: a
       gallery does not crop the work to fit a box it chose. */
    aspect-ratio: 4 / 5;
}

.art-stack__frame img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    transition: transform .6s cubic-bezier(.22, 1, .36, 1);
}

.art-stack__frame:hover img {
    transform: scale(1.02);
}

/* ── the caption, under the picture ──
   The grid used to absolutely position this over the bottom of every painting
   on a dark scrim, which covered roughly the lower 40% of the work. Nothing is
   laid over the art here. */
.art-stack__cap {
    padding-top: 0.85rem;
}

.art-stack__title {
    font-family: var(--font-display);
    font-size: 1.05rem;
    line-height: 1.25;
    margin: 0 0 0.2rem;
}

.art-stack__title a {
    text-decoration: none;
    color: inherit;
}

.art-stack__meta {
    font-size: 0.8125rem;
    opacity: 0.7;
    margin: 0 0 0.25rem;
}

.art-stack__price {
    font-size: 0.9rem;
    margin: 0 0 0.6rem;
}

.art-stack__price .from {
    font-size: 0.75rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.65;
}

/* The named call the brief asked for. A real link with real words, not a hover
   affordance — on a phone there is no hover, and "click the picture" is not
   something a page should expect a visitor to already know. */
.art-stack__cta {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    font-size: 0.75rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    text-decoration: none;
    padding-bottom: 4px;
    border-bottom: 1px solid currentColor;
    transition: opacity .3s, gap .3s;
}

.art-stack__cta:hover {
    opacity: 0.65;
    gap: 0.7em;
}

.art-stack__arrow {
    position: absolute;
    top: calc(50% - 2.5rem);
    transform: translateY(-50%);
    z-index: 2;
    display: grid;
    place-items: center;
    width: 2.5rem;
    height: 2.5rem;
    border: 0;
    border-radius: 50%;
    cursor: pointer;
    background: var(--paper, #fff);
    color: var(--ink, #111);
    box-shadow: 0 2px 14px rgba(0, 0, 0, .18);
    opacity: 0;
    transition: opacity .3s;
}

.art-stack__viewport:hover .art-stack__arrow,
.art-stack__arrow:focus-visible {
    opacity: 1;
}

/* Touch has no hover to reveal them, and the track is already swipeable there,
   so the arrows would only ever be in the way. */
@media (hover: none) {
    .art-stack__arrow {
        display: none;
    }
}

.art-stack__arrow--prev { left: 0.6rem; }
.art-stack__arrow--next { right: 0.6rem; }

.art-stack__arrow[disabled] {
    opacity: 0 !important;
    pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
    .art-stack__track { scroll-behavior: auto; }
    .art-stack__frame img,
    .art-stack__cta { transition: none; }
    .art-stack__frame:hover img { transform: none; }
}

/* The flat card's "View This Painting", matching the stacks' own CTA so a
   search result and a series read the same way. Inherits rather than fixing a
   colour: this now sits on the page, not on the scrim that used to be here. */
.art-card__cta {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    margin-top: 0.5rem;
    font-size: 0.7rem;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    text-decoration: none;
    color: inherit;
    padding-bottom: 3px;
    border-bottom: 1px solid currentColor;
    transition: opacity .3s, gap .3s;
}

.art-card__cta:hover {
    opacity: 0.65;
    gap: 0.7em;
}

@media (prefers-reduced-motion: reduce) {
    .art-card__cta { transition: none; }
}
