/* =============================================================================
   Andipa Editions — Banksy Editions Landing Page
   -----------------------------------------------------------------------------
   Design language: a dealer's gallery, not a shop. Warm paper ground, near-black
   ink, high-contrast display serif against a neutral grotesque, hairline rules,
   and a lot of air. Colour is structural, never decorative.

   Rules of the house — keep these and the page stays coherent:
     1. The page is LIGHT. Separation comes from three tonal steps — white,
        --paper-2 and --paper-3 — plus hairlines, never from darkness. Dark is
        reserved for text, primary buttons and UI chrome (sticky bar, lightbox).
        Never reintroduce a mid-brown; muddy mid-tones are what read as template.
     2. The artwork is always the brightest, largest thing in its section, and
        always carries a hairline frame — see --frame.
     3. Type does the hierarchy. There are no shadows anywhere in this file,
        deliberately; depth comes from tonal steps and hairlines alone.
     4. Accent red appears only on required marks, errors and focus. Never decor.
     5. Every text/ground pair must clear WCAG AA. The audience is 55+; see
        DOCUMENTATION.md §9 for the measured ratios.

   Mobile-first. Breakpoints at 720px and 1000px.
   ============================================================================= */

:root {
    /* --- Grounds ------------------------------------------------------------ */
    --paper:     #fbfaf7;   /* warm alabaster — stark white reads clinical, not costly */
    --paper-2:   #f3efe7;   /* tonal band: alternating sections, mounts, fields */
    --paper-3:   #eae4d8;   /* deepest tone: footer */
    /* Dark is UI chrome AND exactly one content band: the film (see .film).
       A page made only of cream reads as stationery however good the type is —
       one near-black section is the cheapest perceived-quality lever available,
       and the film is the right one to spend it on, because a film wants a dark
       room. Two bands only, paper and noir; nothing between. */
    --noir:      #1c1813;
    --noir-2:    #1d1b18;
    /* The film band, changed from --noir on 2026-09-07. Within a hair of
       --paper-2 (#f3efe7) and deliberately its own token rather than reusing it:
       the film is the one section anybody is likely to want to re-tone again, and
       when they do it should be one line here, not a hunt through the section. */
    --film-band: #f4ede7;

    /* --- Ink ---------------------------------------------------------------- */
    --ink:       #1c1813;   /* warm espresso, not pure black — 16.9:1 */
    --ink-2:     #5e5648;   /* secondary text — 6.9:1 on paper, 6.3:1 on the band */
    --on-noir:   #f2efe9;   /* primary text on noir */
    --on-noir-2: #b0a99e;   /* secondary text on noir — 7.6:1 */

    /* --- Structure ---------------------------------------------------------- */
    --line:      #e6dfd0;   /* hairline on paper */
    --line-2:    #d8cfba;   /* hairline on a tonal band — darker, or it disappears */

    /* --- The luxury accent: antique brass ------------------------------------
       Every tone below is measured, not picked by eye:
         --gilt        #836636  BUTTON FILL, carrying alabaster text — 5.13:1.
                                Gold-with-black text reads as a dense dark block;
                                bronze with a light label reads lighter and far
                                more expensive. Also 5.13:1 against the page, so
                                the button's own edge is perceivable
                                (WCAG 1.4.11 non-text contrast).
         --gilt-deep   #6e5529  button hover — DEEPENS, since the label is light.
         --gilt-bright #a8813f  decorative accents only: the rule under the
                                headline, nav underlines, a warmed artwork frame.
                                Too light to sit behind text.
         --gilt-text   #7a5f2e  brass dark enough to BE text — 5.7:1 on paper.
         --frame       #c3a877  resting hairline around every artwork.
       Brass is the point: pure black on white reads efficient. It does not read
       like somewhere you spend £75,000. */
    --gilt:        #836636;
    --gilt-deep:   #6e5529;
    --gilt-bright: #a8813f;
    --gilt-text:   #7a5f2e;
    --frame:       #c3a877;

    /* --- The same two accents, at a lightness that survives the noir band -----
       Brass and the house red are both mixed for a paper ground; on #1c1813 the
       eyebrow brass falls to 4.9:1 and the red to 2.4:1, which fails outright.
       These are the SAME two hues re-mixed for the dark, not new colours:
         --gilt-on-noir    #c9a25a  7.4:1 on noir — the eyebrow, heading rules
         --accent-on-noir  #d9694f  5.1:1 on noir — the film kicker
       Measured by tools/verify-contrast.js, which now walks the dark band too. */
    --gilt-on-noir:   #c9a25a;
    --accent-on-noir: #d9694f;
    /* Sampled from the logo artwork, so the brand red is ONE colour rather than
       two that are almost the same. 7.02:1 on paper. Note it sits at only 1.37:1
       against --gilt: near-identical in value, so never place red and brass
       adjacent at similar size — they turn to mud. */
    --accent:    #993420;
    /* Must stay in step with --accent above. Deliberately a literal rgba() rather
       than color-mix(): color-mix has no fallback in older browsers, and this is
       an error state — the one place a silently-dropped declaration is least
       acceptable. They are adjacent here so they get changed together. */
    --accent-tint: rgba(153, 52, 32, .08);              /* required marks, errors, focus only */

    /* --- Type --------------------------------------------------------------- */
    --serif: "Cormorant Garamond", Georgia, "Times New Roman", serif;
    --sans:  "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;

    /* --- Layout ------------------------------------------------------------- */
    --wrap:      1280px;
    --gutter:    clamp(22px, 5vw, 72px);
    --section-y: clamp(64px, 9.5vw, 150px);   /* vertical rhythm between sections */
    --gap:       clamp(20px, 2.4vw, 34px);

    /* The sticky header's measured height, used by .first-screen to fill exactly
       the rest of the viewport. It is an assumption, so tools/verify-responsive.js
       measures the real header at every width and fails if it drifts from this. */
    --header-h: 81px;

    /* Height reserved for the sticky header when jumping to an anchor.
       Used by html{scroll-padding-top} below, which BOTH the native anchor jump
       and Lenis's scrollTo() respect — so the offset lives in exactly one place.
       Measured header is 81px at every breakpoint (390/768/1024/1440); the extra
       15px keeps the target from sitting flush against the header edge. */
    --anchor-offset: 96px;

    /* Distance from the centred .wrap's content edge out to the viewport edge.
       Lets a section break the grid and run full-bleed without magic numbers.
       Only ever used inside an overflow-x: clip container, so the scrollbar's
       few px of 100vw inaccuracy can never produce a horizontal scrollbar. */
    --bleed: calc(max(0px, (100vw - var(--wrap))) / 2 + var(--gutter));
    /* The hero panel's inner padding on desktop. It is a token because the header
       aligns to it: the Enquire button's right edge and the hero's caption rule are
       the same vertical line, and they must stay that way if this number changes. */
    --hero-pad: clamp(36px, 4vw, 64px);

    /* --- Motion ---------------------------------------------------------------
       Two curves only. --ease for layout movement; --ease-lux for anything a
       visitor triggers by hovering. --ease-lux is expo-out: it moves immediately,
       then takes its time settling. That long tail is the difference between a
       control that merely responds and one that feels considered — and it is why
       the hover durations below are ~.6s rather than the usual .2s. Fast hovers
       read as software; slow ones read as material. */
    --ease:     cubic-bezier(.22, 1, .36, 1);
    --ease-lux: cubic-bezier(.16, 1, .3, 1);
}

/* ============================== SCROLLBAR ==============================
   The one piece of browser furniture that sits on the page all the time. Default
   grey on warm paper is the same jarring note as a stock blue link — this is a
   brass thumb on the page's own ground, square like everything else here.
   Deliberately NOT hidden or made hair-thin: the audience is 55+, and a scrollbar
   they cannot grab is a worse crime than an ugly one. Firefox gets the two-value
   `scrollbar-color`; WebKit and Chromium get the pseudo-elements. */
html {
    scrollbar-color: var(--gilt) var(--paper-2);
    scrollbar-width: auto;
}
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--paper-2); }
::-webkit-scrollbar-thumb {
    background: var(--gilt);
    border: 3px solid var(--paper-2);   /* inset, so the thumb reads as a slim bar */
}
::-webkit-scrollbar-thumb:hover { background: var(--gilt-deep); }
::-webkit-scrollbar-corner { background: var(--paper-2); }

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

/* A section's own top padding (up to 150px) sat between the header and its
   heading after an anchor jump, so "The Collection" arrived a third of a screen
   down with nothing above it. Negative scroll-margin cancels that padding and
   leaves a deliberate 28px. Both the native jump and Lenis respect it, so the
   offset still lives in exactly one place.
   NB the hero is excluded: it has no such padding and is the top of the page. */
.section[id] { scroll-margin-top: calc(28px - var(--section-y)); }

html {
    -webkit-text-size-adjust: 100%;
    font-size: 18px;                 /* scales the whole system up for an older audience */
    /* Fallback only: active when JS is off or Lenis fails to load.
       Lenis disables it via html.lenis below to avoid a double animation. */
    scroll-behavior: smooth;
    scroll-padding-top: var(--anchor-offset);
}

body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: var(--sans);
    font-size: 1rem;
    font-weight: 400;
    line-height: 1.68;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
    font-synthesis-weight: none;
    /* Room for the sticky mobile bar, including the iOS home indicator. One rail
       of 44px plus its 9px of padding, not the 104px slab it used to be. */
    padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px));
}
/* The sticky bar's clearance. Tied to the bar, and the bar is tied to the nav
   row — see the 1100px block. */
@media (min-width: 1100px) { body { padding-bottom: 0; } }

/* ============================== THE PAGE ARRIVES ==============================
   A fade, and nothing else. No spinner and no percentage: those are for pages
   that make you wait, and announcing a wait is how you create one.

   `page-enter` is set by the inline script at the top of index.php, before the
   first paint, and removed a frame after the document is parsed. It is set by
   SCRIPT rather than written into the HTML, so with scripting off the page is
   simply visible — CSS alone can never leave a visitor looking at a blank page.

   The transition lives on <body> unconditionally so it is ready the moment the
   class lifts. An element at opacity < 1 is the containing block for any fixed
   descendant, which would matter if an overlay could be open — none can be, half
   a second into a page load, and at rest the body is at opacity 1 where the rule
   does not apply. */
html.page-enter body { opacity: 0; }
body { opacity: 1; transition: opacity .55s var(--ease-lux); }
@media (prefers-reduced-motion: reduce) {
    html.page-enter body { opacity: 1; }
    body { transition: none; }
}

/* --- Paper grain -------------------------------------------------------------
   Flat cream is what makes a screen look like a screen. Real paper has tooth,
   and three per cent of it is the difference between a colour and a material —
   you are not meant to see this, only to stop seeing the flatness.

   Generated by feTurbulence in an inline data URI: no request, ~400 bytes, and
   `stitchTiles` makes the 180px tile seamless. Fixed rather than scrolling, the
   way grain sits on film rather than on what is filmed.

   It hangs off <html>, AFTER <body> in tree order, so it paints over ordinary
   page content but UNDER anything that establishes its own stacking context —
   the header, the sticky bar, the lightbox, the viewer, the film modal. That is
   deliberate: a collector inspecting a work at full zoom should be looking at
   the work, not through three per cent of anything. */
html::after {
    content: "";
    position: fixed; inset: 0; z-index: 0;
    pointer-events: none;
    opacity: .035;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23g)'/%3E%3C/svg%3E");
}
/* Print has its own tooth. */
@media print { html::after { display: none; } }

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }
button { font: inherit; color: inherit; }

::selection { background: var(--ink); color: var(--paper); }

/* Double-tapping a control is a double tap, not a request to zoom the page. On the
   lightbox arrows especially — where a visitor pages through works with two quick
   taps in the same spot — iOS was zooming the whole screen. This also drops the
   ~300ms delay Safari holds on every tap while it waits for a second one.
   Pinch-zoom on the page itself is untouched. */
button, a, summary, [role="button"] { touch-action: manipulation; }

/* One focus style everywhere. Visible, not ugly. */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter); }
.section { padding-block: var(--section-y); }

.skip-link {
    position: absolute; left: -9999px; top: 0; z-index: 200;
    background: var(--ink); color: var(--paper);
    padding: 14px 22px; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase;
}
.skip-link:focus { left: 0; }

/* Text for a screen reader and nobody else. Clip, not display:none — display:none
   removes it from the accessibility tree too, which defeats the point. */
.visually-hidden {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; overflow: hidden; white-space: nowrap;
    clip: rect(0 0 0 0); clip-path: inset(50%); border: 0;
}

/* ============================== TYPE ============================== */
h1, h2, h3, h4 {
    font-family: var(--serif);
    font-weight: 400;
    margin: 0;
    letter-spacing: -.012em;
    text-wrap: balance;
}

h1 {
    /* Min deliberately restrained: at 18px root a larger floor pushed the artwork
       clean off the first mobile screen, and the product is the point. */
    font-size: clamp(2.5rem, 6.2vw, 5rem);
    line-height: .95;
    letter-spacing: -.022em;
}
h2 {
    font-size: clamp(1.95rem, 3.9vw, 3rem);
    line-height: 1.07;
}
h3 { font-size: clamp(1.3rem, 2.1vw, 1.7rem); line-height: 1.15; }

/* A short brass rule under every section heading. The hero's headline already
   had one (the <hr class="rule">), and repeating it at every h2 is what turns a
   one-off flourish into a house style — it is the same gesture as the rule above
   each footer column heading and the underline on the active filter.
   Excluded: the footer's column headings (they carry the rule ABOVE instead) and
   any h2 that is not a section title. */
.section h2::after {
    content: ""; display: block;
    width: 48px; height: 1px;
    margin-top: clamp(.55em, 1.4vw, .8em);
    background: var(--gilt-bright);
}
.site-footer h2::after { content: none; }
h4 { font-size: 1.15rem; line-height: 1.25; }

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

/* The small letterspaced label that sits above a heading. Sets the register
   for the whole page — it is the difference between "gallery" and "shop". */
.eyebrow {
    font-family: var(--sans);
    font-size: .66rem;
    font-weight: 500;
    letter-spacing: .26em;
    text-transform: uppercase;
    color: var(--gilt-text);        /* brass, not grey — the accent runs page-wide */
    margin: 0 0 1.4em;
}

.lede {
    font-size: clamp(1.02rem, 1.3vw, 1.14rem);
    line-height: 1.62;
    color: var(--ink-2);
}

.centered { text-align: center; }

/* Short hairline used as a typographic break under a heading. */
.rule {
    width: 54px; height: 0;
    border: 0; border-top: 1px solid var(--gilt-bright);
    margin: 1.5rem 0 1.9rem;
}
.centered + .rule, .rule.centered { margin-inline: auto; }

/* ============================== BUTTONS & LINKS ============================== */
/* ONE button scale for the whole page. The old 17px/34px at .7rem made the hero
   CTA read as a slab; this is the same shape at a more tailored weight. Anything
   that needs to be smaller (the header CTA) reduces from here rather than
   inventing its own numbers — that is what keeps them consistent.
   Height lands at ~44px, which is also the comfortable touch target. */
.btn {
    display: inline-flex; align-items: center; justify-content: center;
    font-family: var(--sans);
    font-size: .65rem; font-weight: 500;
    letter-spacing: .16em; text-transform: uppercase;
    line-height: 1.35;          /* labels wrap on narrow screens; 1 jammed the lines */
    text-align: center;
    padding: 15px 30px;
    border: 1px solid transparent;
    text-decoration: none;
    cursor: pointer;
    /* The sweep-fill needs a stacking context of its own: isolation:isolate lets
       the ::before sit at z-index:-1 — behind the label, still above the
       button's own background. */
    position: relative; isolation: isolate; overflow: hidden;
    transition: color .55s var(--ease-lux), border-color .55s var(--ease-lux);
}
/* The fill sweeps across rather than switching. One gesture, used by every
   button on the page. */
.btn::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    transform: scaleX(0); transform-origin: left center;
    transition: transform .6s var(--ease-lux);
}
.btn:hover::before,
.btn:focus-visible::before { transform: scaleX(1); }
/* Primary = antique brass with espresso text. A black rectangle is what every
   template ships with; brass is the loudest luxury signal available without
   touching the type. Hover LIGHTENS, like light moving across metal. */
.btn-dark            { background: var(--gilt); color: var(--paper); border-color: var(--gilt); }
.btn-dark::before    { background: var(--gilt-deep); }
.btn-dark:hover      { border-color: var(--gilt-deep); color: var(--paper); }
.btn-outline         { background: transparent; color: var(--ink); border-color: var(--ink); }
.btn-outline::before { background: var(--ink); }
.btn-outline:hover   { color: var(--paper); }
.btn-outline-light         { background: transparent; color: var(--on-noir); border-color: rgba(242,239,233,.42); }
.btn-outline-light::before { background: var(--on-noir); }
.btn-outline-light:hover   { color: var(--noir); border-color: var(--on-noir); }
/* For a light ground: ink outline, filling with ink on hover. The -light pair
   above is for the lightbox and anything else on a dark panel. */
.btn-outline-dark         { background: transparent; color: var(--ink); border-color: var(--line-2); }
.btn-outline-dark::before { background: var(--ink); }
.btn-outline-dark:hover   { color: var(--paper); border-color: var(--ink); }
/* Brass fill — the primary action wherever the ground is paper. */
.btn-gilt         { background: var(--gilt); color: var(--paper); border-color: var(--gilt); }
.btn-gilt::before { background: var(--gilt-deep); }
.btn-gilt:hover   { color: var(--paper); border-color: var(--gilt-deep); }

.btn-block { width: 100%; }
/* Buttons are set for a desktop, where 30px of side padding and .16em of tracking
   read as generous. On a phone the same figures make a full-width label look like
   a banner: the label is a third of the width and the rest is air. These are the
   same proportions at a phone's scale — smaller, tighter, still a 44px target. */
@media (max-width: 619px) {
    .btn { font-size: .6rem; letter-spacing: .13em; padding: 14px 24px; }
}

/* Text link with an arrow and a rule that draws itself on hover. */
.link-arrow {
    display: inline-block; position: relative;
    font-family: var(--sans);
    font-size: .7rem; font-weight: 500;
    letter-spacing: .17em; text-transform: uppercase;
    text-decoration: none; color: var(--ink);
    padding-bottom: 9px;
}
.link-arrow { transition: color .5s var(--ease-lux), transform .5s var(--ease-lux); }
.link-arrow::after {
    content: ""; position: absolute; left: 0; bottom: 0;
    width: 100%; height: 1px; background: var(--ink);
    transition: background-color .5s var(--ease-lux);
}
/* The link moves toward where it is taking you, and its rule turns to brass.
   The previous behaviour retracted the rule on hover, which read as withdrawal. */
.link-arrow:hover { color: var(--gilt-text); transform: translateX(5px); }
.link-arrow:hover::after { background: var(--gilt-bright); }

/* ============================== STAGING RIBBON ==============================
   Rendered only on staging.* (andipa_env() in config.php), and deliberately the
   one ugly element on the page. Its whole job is to be impossible to mistake for
   part of the design — the moment it looks at home, somebody screenshots a
   preview and sends it round as the live site.

   Above the header and in normal flow, so it pushes the page down rather than
   covering anything: a banner that overlays a sticky header is a banner people
   learn to scroll past. */
.staging-flag {
    margin: 0; padding: 9px clamp(14px, 3vw, 28px);
    background: #1c1813; color: #f6f4ef;
    font-family: var(--sans); font-size: .72rem; line-height: 1.5;
    letter-spacing: .04em; text-align: center;
    border-bottom: 3px solid #d9694f;
}
.staging-flag strong {
    font-weight: 600; letter-spacing: .16em; text-transform: uppercase;
    color: #d9694f; margin-right: .5em;
}

/* ============================== HEADER ============================== */
/* The header hides on the way DOWN from the film onwards and returns on the way
   up — see main.js §0f. The transition lives here so it also covers the way back. */
body.header-away .site-header { transform: translateY(-100%); opacity: 0; }
.site-header {
    transition: transform .55s var(--ease-lux), opacity .3s var(--ease-lux);
    position: sticky; top: 0; z-index: 100;
    background: rgba(246,244,239,.88);
    -webkit-backdrop-filter: saturate(140%) blur(10px);
    backdrop-filter: saturate(140%) blur(10px);
    border-bottom: 1px solid var(--line);
}
.header-inner { display: flex; align-items: center; justify-content: space-between; padding-block: 18px; gap: 20px; }
/* The tightest the header ever is: just past the breakpoint where the burger
   menu becomes a seven-item row, and before the viewport is wide enough to give
   the row any slack. At 1000px exactly it was over by 4px — a horizontal
   scrollbar on the whole page for the sake of the space between the wordmark and
   the first link. The gap is the right thing to spend: nothing sits in it. */
@media (min-width: 1100px) and (max-width: 1299px) {
    .header-inner { gap: 12px; }
}

.logo { display: block; flex: 0 0 auto; }
/* The wordmark is a single line — the previous lockup stacked "Andipa" over
   "EDITIONS", so it needed more height to give the word the same presence.
   44px here renders the word slightly LARGER than before despite the smaller
   number. Aspect is 3.61:1, so this is ~159px wide; check the nav still fits
   at 1000-1150px if you change it. */
.logo img { width: auto; height: 44px; }

.nav-toggle {
    display: inline-flex; flex-direction: column; justify-content: center; gap: 5px;
    width: 44px; height: 44px; padding: 10px; margin-right: -10px;
    background: none; border: 0; cursor: pointer;
}
.nav-toggle span { display: block; width: 24px; height: 1px; background: var(--ink); transition: transform .3s var(--ease), opacity .2s; }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

.site-nav {
    position: absolute; top: 100%; left: 0; right: 0;
    display: none; flex-direction: column; align-items: stretch;
    background: var(--paper); border-bottom: 1px solid var(--line);
    padding: 8px var(--gutter) 22px;
}
/* Open, the menu is the whole screen below the header — not a short panel with
   the page showing underneath, which read as a dropdown rather than a place you
   had arrived at. dvh, not vh: it must track the phone's browser chrome as it
   hides, or the last item sits under the toolbar. The vh line is the fallback.
   NB it is `absolute`, not `fixed`: .site-header has a backdrop-filter, which
   makes it the containing block for fixed descendants, so `fixed` would be
   measured against the header — a 81px-tall menu. The header is sticky, so
   absolute + top:100% follows it anyway. */
/* The panel drops in and its rows follow, a beat apart. Opening a full-screen menu
   with `display: flex` alone reads as a jump cut; 60ms of stagger is the difference
   between a panel appearing and a panel arriving. The global prefers-reduced-motion
   rule at the foot of this file collapses all of it. */
/* The panel is WIPED down, not faded in. Fading it cross-dissolved the hero
   through the menu for a third of a second, which read as a rendering fault;
   the panel is opaque on every frame here and simply arrives from the header
   edge, which is how a physical blind would do it. Transform is out of the
   question: the panel is absolutely positioned to the sticky header, so any
   upward offset slides it under the header and it peeks out at the wrong size. */
@keyframes nav-panel-in {
    from { clip-path: inset(0 0 100% 0); }
    to   { clip-path: inset(0 0 0 0); }
}
@keyframes nav-row-in {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}
.site-nav.is-open {
    animation: nav-panel-in .42s var(--ease-lux) both;
    height: calc(100vh  - var(--header-h));
    height: calc(100dvh - var(--header-h));
    overflow-y: auto; overscroll-behavior: contain;
    padding-bottom: calc(22px + env(safe-area-inset-bottom, 0px));
}
/* Enquire drops to the foot of the panel: the links are a list to scan, the CTA
   is where the thumb rests. */
.site-nav.is-open > .nav-cta { margin-top: auto; }

.site-nav.is-open > a,
.site-nav.is-open > .nav-foot { animation: nav-row-in .5s var(--ease-lux) both; }
.site-nav.is-open > *:nth-child(1) { animation-delay: .06s; }
.site-nav.is-open > *:nth-child(2) { animation-delay: .12s; }
.site-nav.is-open > *:nth-child(3) { animation-delay: .18s; }
.site-nav.is-open > *:nth-child(4) { animation-delay: .24s; }
.site-nav.is-open > *:nth-child(5) { animation-delay: .30s; }
.site-nav.is-open > *:nth-child(6) { animation-delay: .36s; }
/* Seven links plus the button now. An item past the last delay falls back to 0s
   and arrives BEFORE the ones above it, which reads as a glitch rather than a
   stagger — so the ladder runs to eight and the foot comes after all of them. */
.site-nav.is-open > *:nth-child(7) { animation-delay: .42s; }
.site-nav.is-open > *:nth-child(8) { animation-delay: .48s; }
.site-nav.is-open > .nav-foot      { animation-delay: .54s; }

/* Closing: the panel leaves the way it came. main.js holds the classes on for the
   length of it rather than switching display off mid-fade. */
.site-nav.is-closing {
    display: flex; pointer-events: none;
    animation: nav-panel-in .26s var(--ease) reverse both;
    /* The geometry has to be repeated here, not inherited from .is-open: that class
       is already gone by the time this one paints, and without it the panel would
       collapse to content height on the first frame and fade out at the wrong size. */
    height: calc(100vh  - var(--header-h));
    height: calc(100dvh - var(--header-h));
    overflow-y: hidden;
    padding-bottom: calc(22px + env(safe-area-inset-bottom, 0px));
}
.site-nav.is-closing > .nav-cta { margin-top: auto; }
.site-nav.is-closing > a,
.site-nav.is-closing > .nav-foot { animation: none; opacity: 1; transform: none; }

/* Below the CTA: whether anyone is there, and where there is. The panel fills the
   screen on a phone and most of it was empty; these are the two things a visitor
   with the menu open is most likely to want next. */
.nav-foot {
    margin-top: 26px; padding-top: 20px;
    border-top: 1px solid var(--line);
    display: flex; flex-direction: column; gap: 10px;
}
.nav-foot-tel {
    font-family: var(--serif); font-size: 1.22rem;
    color: var(--ink); text-decoration: none;
    font-variant-numeric: tabular-nums;
}
.nav-foot-address {
    font-style: normal; font-size: .8rem; line-height: 1.6; color: var(--ink-2);
}
/* Moves with the row above — see the 1100px block. Below it the panel is
   what opens, and the panel needs its foot. */
@media (min-width: 1100px) { .nav-foot { display: none; } }
/* The sticky Call/Enquire bar would sit ON TOP of the open menu (it is fixed,
   the menu is not) and repeats what the panel already offers. */
body.nav-open .mobile-cta { display: none; }
.site-nav.is-open { display: flex; }
.site-nav > a {
    font-family: var(--sans);
    font-size: .72rem; font-weight: 500;
    letter-spacing: .16em; text-transform: uppercase;
    text-decoration: none; color: var(--ink);
    padding: 15px 0; border-bottom: 1px solid var(--line);
    transition: color .3s var(--ease);
}
.site-nav > a:last-child { border-bottom: 0; }
.site-nav > a:hover { color: var(--ink-2); }

/* WHERE YOU ARE. The panel now stays open while a visitor works through it, so it
   has to answer "which one did I just press?" — otherwise a menu that does not
   disappear is just a menu you have lost your place in. Brass, and a marker in the
   left margin rather than a background: the panel is paper, and a filled row would
   be the only block of colour on it. */
.site-nav > a.is-current { color: var(--gilt-text); }
.site-nav > a.is-current::before {
    content: ""; position: absolute; left: -14px; top: 50%;
    width: 6px; height: 6px; margin-top: -3px; border-radius: 50%;
    background: var(--gilt);
}
.site-nav > a { position: relative; }

/* Off-site link — deliberately demoted so it doesn't compete with the in-page nav. */
.nav-external {
    display: inline-flex; align-items: center; gap: 5px;
    color: var(--ink-2) !important;
    font-size: .62rem !important;
}
.nav-external:hover { color: var(--ink) !important; }
.nav-external span { font-size: .9em; }

/* The page's one conversion goal, promoted to a button in the header.
   Two class selectors so it outranks `.site-nav > a`, which zeroes padding. */
.site-nav > .nav-cta {
    background: var(--gilt); color: var(--paper);
    border: 1px solid var(--gilt);
    justify-content: center;
}
/* Only the desktop header gets the reduced size — in the mobile dropdown it keeps
   the full .btn padding so it stays a comfortable touch target. */
@media (min-width: 1100px) {
    .site-nav > .nav-cta { padding: 12px 24px; font-size: .6rem; }
}
.site-nav > .nav-cta::before { background: var(--gilt-deep); }
.site-nav > .nav-cta:hover { border-color: var(--gilt-deep); color: var(--paper); }

/* ============================== HERO ==============================
   A lit artwork on a gallery wall, not two boxes floating on cream.

   Three moves carry it:
   1. The right column is a NOIR panel that bleeds off the right edge of the
      viewport and runs the full height of the section — header to trust strip.
      That kills the dead space the old centred layout left in the middle and
      gives the page its one moment of real contrast.
   2. The print sits directly on that dark ground with no mount. The old
      --paper-2 mount was within a few percent of the page ground AND of the
      print's own paper, so three near-identical creams stacked up and the work
      looked washed out. The panel is now a distinct tonal step AND every print
      carries a hairline frame, so even a near-white paper still reads as an object.
   3. The print is sized by viewport HEIGHT, so it stays large on a desktop and
      still fits a 600px laptop window.

   The bleed uses a ::before with a large negative offset, contained by
   overflow-x: clip on the section — `clip` does not create a scroll container,
   so it cannot introduce a horizontal scrollbar or break sticky positioning.
   ------------------------------------------------------------------------- */
.hero {
    /* No bottom padding: below 900px the artwork panel is a full-width tonal
       band, and page-coloured padding under it read as an unfinished gap above
       the trust strip. The panel carries its own inner padding, so the caption
       still has air. Desktop restores nothing — it already sets padding-block:0. */
    padding-block: clamp(36px, 4.5vh, 88px) 0;
    overflow-x: clip;                 /* contains the bleeding panel */
}
.hero-grid {
    display: grid; grid-template-columns: 1fr;
    gap: clamp(30px, 4vw, 68px);
    align-items: stretch;
}

.hero-copy { display: flex; flex-direction: column; justify-content: center; }
/* The hero headline is sized to ITS OWN line length, not to the global h1 scale.
   "Where collectors / buy their Banksy" is 16 characters a line against the old
   headline's 13, and the copy column stops growing at --wrap while a vw-based
   size does not — so at 1440px and above the global clamp broke a two-line
   headline into four. Roughly 6.9em is the widest line here; these numbers leave
   it about 30px of slack in the narrowest column at every width.
   CHANGE THE HEADLINE, RE-CHECK THIS. verify-responsive.js will catch the
   overflow at 320px but not a headline quietly wrapping to three lines. */
.hero h1 { font-size: clamp(2.15rem, 5.6vw, 4.5rem); }
.hero-copy .eyebrow { margin-bottom: 1.5em; }
.hero-copy .lede { max-width: 38ch; }
.hero-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 26px; margin-top: 2.2rem; }
/* On a phone the button belonged to the trust strip rather than to the sentence
   above it: 61px of air above it and none at all below, so it read as the strip's
   header. It now sits closer to the copy it answers, and the hero keeps some
   ground of its own underneath — the strip is a separate band and should look
   like one. (Desktop is untouched: there the copy column is centred against a
   full-height panel and the spacing is doing a different job.) */
@media (max-width: 899px) {
    .hero-actions { margin-top: 1.5rem; }
    .hero { padding-bottom: clamp(30px, 4.5vh, 56px); }
}

.hero-call {
    font-family: var(--sans); font-size: .72rem; font-weight: 500;
    letter-spacing: .13em; text-transform: uppercase;
    color: var(--ink-2); text-decoration: none;
    border-bottom: 1px solid var(--line); padding-bottom: 4px;
    transition: color .3s var(--ease), border-color .3s var(--ease);
}
.hero-call { transition: color .5s var(--ease-lux), border-color .5s var(--ease-lux); }
.hero-call:hover { color: var(--gilt-text); border-color: var(--gilt-bright); }

/* --- the panel --- */
.hero-media {
    position: relative;
    margin: 0;
    /* Declared on the element as well as the bleeding ::before. Visually identical,
       but it means contrast checkers and screen-reader tooling resolve the real
       background instead of walking up to the page's paper ground. */
    background: var(--paper-2);
    display: flex; flex-direction: column; justify-content: center;
    padding: clamp(30px, 4.5vh, 60px) clamp(24px, 3.2vw, 46px) clamp(24px, 3.5vh, 44px);
}
/* Below 900px the plate does not appear at all.
   On a phone it was a full screen of scrolling between the headline and the film,
   and the visitor had to travel past a picture they could not act on to reach the
   works they could. The film's poster is the next thing they meet instead — also a
   Banksy, also a picture, and one that leads somewhere. The desktop layout is
   untouched: there the plate sits BESIDE the copy and costs no vertical distance
   at all, which is the whole reason it works there and not here. */
@media (max-width: 899px) {
    .hero-media { display: none; }
}
.hero-media::before {
    content: ""; position: absolute; z-index: 0;
    top: 0; bottom: 0;
    /* Mobile: bleed both edges for a full-width band. Overridden at 900px. */
    left: calc(-1 * var(--gutter)); right: calc(-1 * var(--gutter));
    background: var(--paper-2);
}
.hero-plate,
.plate-caption { position: relative; z-index: 1; }

/* The plate is a BUTTON — clicking the work on the wall opens that same edition
   in the lightbox (main.js §0d). So it needs a button reset, and it borrows the
   collection card's hover language exactly: the work drifts a little, its frame
   warms to brass, and the "View" pill rises. Without that it reads as decoration
   and nobody discovers it is clickable. */
.hero-plate {
    -webkit-appearance: none; appearance: none;
    border: 0; background: none; padding: 0; margin: 0;
    font: inherit; color: inherit; width: 100%;
    cursor: zoom-in;                       /* same promise as the collection plates */
    display: flex; align-items: center; justify-content: center;
}
.hero-plate:hover img { transform: scale(1.02); border-color: var(--gilt-bright); }
.hero-plate img {
    transition: transform 1.2s var(--ease-lux), border-color .6s var(--ease-lux);
    width: auto; height: auto;
    max-width: 100%;
    /* Height-led so the work stays large on desktop and still fits a short window */
    max-height: min(54vh, 540px);
    object-fit: contain;
    /* Mat + frame. Without this a pale print dissolves into the panel. */
    background: #fff;
    padding: clamp(10px, 1.4vw, 18px);
    border: 1px solid var(--frame);
}

.plate-caption {
    /* Centred, to share the print's axis. Left-aligned under a centred image is
       the kind of half-alignment that reads as unconsidered. */
    display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center;
    margin-top: clamp(20px, 2.5vh, 30px);
    padding-top: 14px;
    border-top: 1px solid var(--line-2);
    font-size: .78rem; color: var(--ink-2);
}
.plate-caption .plate-title {
    font-family: var(--serif); font-style: italic; font-size: 1.1rem;
    color: var(--ink);
}
/* Middot separators rather than bare gaps — reads as a catalogue line. */
.plate-caption span + span { margin-left: 12px; }
.plate-caption span + span::before {
    content: "\00b7"; margin-right: 12px; opacity: .55;
}

/* ============================== TRUST STRIP ============================== */
.trust-strip { border-block: 1px solid var(--line); }
.trust-inner {
    display: grid; grid-template-columns: 1fr; gap: 0;
    text-align: center;
}
.trust-inner span {
    font-family: var(--sans);
    font-size: .66rem; font-weight: 500;
    /* .2em tracking pushed the longest label onto two lines at 1000-1200px, where
       the columns are narrowest after the strip goes horizontal. */
    letter-spacing: .16em; text-transform: uppercase;
    color: var(--ink-2);
    padding: 20px 8px;
    border-bottom: 1px solid var(--line);
}
.trust-inner span:last-child { border-bottom: 0; }

/* ============================== COLLECTION ============================== */
.collection-intro { max-width: 62ch; margin-bottom: clamp(32px, 4.5vw, 68px); }
.collection-intro h3 { margin-bottom: .7em; }
/* The section is titled, then divided. The header names the whole thing; the two
   block headings sit a clear step below it but well above default h3 scale, which
   read as sub-labels on a phone. */
.collection-header { margin-bottom: clamp(34px, 4vw, 56px); }
.collection-header .eyebrow { color: var(--ink-2); margin-bottom: 1.1em; }
.collection-header h2 + * { margin-top: 0; }
.collection-intro a { color: var(--ink); text-underline-offset: 3px; }


/* --- the filter -----------------------------------------------------------
   Three quiet text buttons on a hairline, not a row of pills: this is a gallery
   label, not an e-commerce facet bar. The active one carries the brass rule. */
.collection-filter {
    display: flex; flex-wrap: wrap; gap: clamp(18px, 3vw, 40px);
    padding-bottom: 14px; margin-bottom: clamp(26px, 3vw, 42px);
    border-bottom: 1px solid var(--line);
}
.filter-btn {
    -webkit-appearance: none; appearance: none;
    background: none; border: 0; padding: 6px 0; margin: 0;
    font-family: var(--sans); font-size: .7rem; font-weight: 500;
    letter-spacing: .17em; text-transform: uppercase;
    color: var(--ink-2); cursor: pointer;
    position: relative;
    transition: color .4s var(--ease-lux);
}
.filter-btn::after {
    content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
    background: var(--gilt-bright);
    transform: scaleX(0); transform-origin: left;
    transition: transform .5s var(--ease-lux);
}
.filter-btn:hover { color: var(--ink); }
.filter-btn.is-active { color: var(--ink); }
.filter-btn.is-active::after,
.filter-btn:hover::after { transform: scaleX(1); }
/* The count is data, not a label — it sits back a step. */
.filter-count { margin-left: .5em; font-size: .62rem; color: var(--ink-2); letter-spacing: .1em; }
.filter-btn.is-active .filter-count { color: var(--gilt-text); }


/* --- the wall ------------------------------------------------------------
   One grid, everything visible. Two carousels meant a visitor swiped through
   five works, scrolled past a heading and swiped through four more — most
   never saw half the collection. Cards are small enough that nine of them are
   a page rather than a scroll. */
/* One column under 430px. Two columns on a 320-390px phone left each card ~130px
   wide, where the titles were technically present and practically unreadable. */
.works-grid {
    display: grid; gap: var(--gap);
    grid-template-columns: 1fr;
    transition: opacity .22s var(--ease);
}
/* The whole wall dips while the filter re-lays it out. Cross-fading individual
   cards through a grid reflow reads as a jumble; one quiet dip reads as intent. */
.works-grid.is-swapping { opacity: 0; }
.work.is-filtered-out { display: none; }

/* THE WHOLE CARD IS THE CONTROL. Only the plate was clickable, so a visitor who
   went for the title — which is what people aim at in a listing — hit nothing, and
   the card gave no sign it was interactive until the cursor crossed the picture.

   NOT done with a stretched pseudo-element, which was the first attempt: the
   plate carries `overflow: hidden` so the image can drift on hover without
   escaping its mount, and an element cannot be clipped by its own parent and also
   cover the parent's siblings. main.js forwards the click instead (§0d), which
   keeps ONE tab stop for one action — a <button> may not contain an <h3>, so a
   second real control would mean a second stop for the same thing.

   The cursor matches the target: the whole card says zoom-in, because the whole
   card opens the work. */
.work { position: relative; cursor: zoom-in; }

/* Equal cards, and the chips on one line across the row. Rat Clock's medium wraps
   to two lines where the others fit on one, so before this the chips sat at four
   different heights across a row and the wall stopped being scannable — the eye
   reads a row of listings by finding one repeated landmark, and the chip is it. */
.work { display: flex; flex-direction: column; }
.work-caption { display: flex; flex-direction: column; flex: 1 1 auto; }

/* Hover and keyboard focus reach the whole card, not just the picture: the title
   takes the same brass underline the header links use, so the thing a visitor is
   about to click is the thing that responds. */
.work:hover .work-title,
.work:focus-within .work-title {
    text-decoration: underline;
    text-decoration-color: var(--gilt);
    text-underline-offset: .18em;
    text-decoration-thickness: 1px;
}
/* The plate is a button with no visible box, so the focus ring has to be drawn
   around the CARD or a keyboard user cannot see where they are. */
.work-media:focus-visible { outline: none; }
.work:focus-within { outline: 2px solid var(--gilt-deep); outline-offset: 6px; }
.work:focus-within:not(:has(:focus-visible)) { outline: none; }

/* The plate. One aspect ratio for every work, whatever its own proportions —
   a panoramic print and a tall portrait sit in identical mounts, which is how
   a catalogue holds together. object-fit:contain never crops the art. */
.work-media {
    /* Flex-centred, not block: the image is sized to its content so the hairline
       frame wraps the ARTWORK, not the plate. */
    /* GRID with an explicit row track, not flex. The plate's height comes from
       aspect-ratio, and WebKit (every iPhone browser) does not resolve the
       image's `max-height: 100%` against a height derived that way — portrait
       works rendered at full width and were clipped at the bottom on iOS while
       Chrome looked perfect. An explicit `minmax(0, 1fr)` row gives the item a
       definite size to measure against. Plain `place-items: center` grid does
       NOT fix it; the row track is the part that matters. */
    display: grid; grid-template-rows: minmax(0, 1fr); place-items: center;
    width: 100%; position: relative;
    aspect-ratio: 1 / 1;
    background: var(--paper-2);
    /* Was clamp(9px, 2.6%, 16px). The mount is what makes these read as a wall of
       framed works, but at 2.6% a panoramic sheet like Applause sat small in a
       large field of paper. Tightened so the WORK is the biggest thing on the
       card, which is the only reason anyone stops at it. */
    border: 0; padding: clamp(7px, 1.7%, 12px);
    /* zoom-in, not pointer: the plate does not navigate, it opens the work at
       full size. Native, so it is correct at every pixel ratio and on every
       platform, and it says what the removed overlay icon used to say without
       putting anything on top of the artwork. */
    cursor: inherit; overflow: hidden;
}
.work-media img {
    width: auto; height: auto;
    max-width: 100%; max-height: 100%;
    object-fit: contain;
    background: #fff;
    padding: clamp(4px, .85vw, 9px);
    border: 1px solid var(--frame);
    transition: transform 1.2s var(--ease-lux), border-color .6s var(--ease-lux);
}
/* Long and small: the work drifts rather than jumps, and its frame warms to
   brass — as though a light had been brought to it. */
.work-media:hover img { transform: scale(1.028); border-color: var(--gilt-bright); }

/* The "View" pill that used to sit on the artwork is gone: on a phone it covered
   the work, and the whole plate is already a button — the cursor, the frame warming
   to brass and the drift on hover say "open me" without printing it over the art. */

/* Caption block. Medium, dimensions and edition size were already in the data
   but never shown — they are exactly what a collector reads first. */
/* Signed or unsigned, ABOVE the plate. It began as a badge over the mount, which
   on the narrowest cards ended up sitting on the artwork itself — and the one
   thing that must never be covered on this page is the work. As a line above the
   plate it also gives every card the same top edge. */
/* Signed / Unsigned, at the FOOT of the caption. It used to sit above the plate
   on its own rule, where it was the first thing read on every card — a one-word
   label carrying more weight than the artwork above it and the title below it.
   Down here it reads as the last line of a catalogue entry, which is what it is.
   No rule of its own: the caption already has one at the top, and a second
   hairline inside a 4-line block starts to look like a table. */
/* Chips, on one row. A bordered chip reads as a property of the work; the bare
   letterspaced line it replaces read as a fourth grey caption line, which is
   exactly what the card had too many of. Position is unchanged — still under the
   edition, at the foot of the entry. */
.work-chips {
    display: flex; flex-wrap: wrap; gap: 6px;
    /* auto, not a fixed top margin: it pins the row to the FOOT of the caption so
       the chips line up across a grid row even when one card's medium wraps to two
       lines — which Rat Clock's does. .7em is the minimum it may sit at. The
       shorthand `margin: .7em 0 0` was here first and quietly overrode the auto
       set higher up the file; both now live in one place. */
    margin: .7em 0 0; margin-top: auto; padding-top: .7em;
}
.chip {
    display: inline-block;
    padding: 4px 9px 3px;
    border: 1px solid var(--line-2); border-radius: 2px;
    font-family: var(--sans); font-size: .55rem; font-weight: 500;
    letter-spacing: .16em; text-transform: uppercase;
    color: var(--ink-2); white-space: nowrap;
    transition: border-color .5s var(--ease-lux), color .5s var(--ease-lux);
}
.work:hover .chip-track { border-color: var(--gilt); color: var(--gilt-text); }
/* Sold is a state, not a property: it takes the house red and a tinted ground so
   it cannot be mistaken for another neutral label in the same row. */
.chip-status {
    border-color: color-mix(in srgb, var(--accent) 45%, transparent);
    background: color-mix(in srgb, var(--accent) 7%, transparent);
    color: var(--accent);
}
.work-caption { margin-top: 13px; padding-top: 11px; border-top: 1px solid var(--line); }
.work-title { font-family: var(--serif); font-style: italic; font-size: 1.1rem; font-weight: 400; margin: 0; line-height: 1.25; }
/* THREE TIERS, NOT ONE. Every line under the title used to be .77-.79rem in
   --ink-2, so a card was a title followed by three interchangeable grey lines and
   the eye had nowhere to land. Now: the year and medium recede (they are the same
   on eight of the nine works and carry almost no information on the wall), and the
   EDITION comes forward in --ink, because the edition and the impression number
   are what a collector is actually scanning for. Catalogue reading order is
   unchanged — only the weight is. */
.work-year  { color: var(--ink-2); }
.work-meta  { font-size: .74rem; color: var(--ink-2); margin: .3em 0 0; line-height: 1.45; }
.work-edition {
    font-size: .82rem; color: var(--ink); margin: .34em 0 0; line-height: 1.4;
    font-variant-numeric: tabular-nums;   /* 124/150 and 69/250 line up down a column */
}
/* A sold work stays on the wall — a dealer's sold work is evidence of trade, and
   hiding it throws that away. It now says so as .chip-status, above; .work-tag and
   .work-status were the two stacked lines the chip row replaced. */
.work-enquire {
    display: inline-block; margin-top: .85em;
    font-family: var(--sans); font-size: .63rem; font-weight: 500;
    letter-spacing: .17em; text-transform: uppercase;
    text-decoration: none; color: var(--ink);
    border-bottom: 1px solid var(--ink); padding-bottom: 4px;
    transition: color .3s var(--ease), border-color .3s var(--ease);
}
.work-enquire { transition: color .5s var(--ease-lux), border-color .5s var(--ease-lux); }
.work-enquire:hover { color: var(--gilt-text); border-color: var(--gilt-bright); }

/* The one link that takes a visitor off the page, centred under the works — see
   the note in index.php. Given its own hairline so it reads as a closing line for
   the section rather than another item in the caption stack. */
.collection-external {
    margin-top: clamp(26px, 3vw, 40px);
    padding-top: clamp(24px, 2.8vw, 36px);
    border-top: 1px solid var(--line);
    /* Left, with the note below it and with everything else on the page. This
       hairline DOES run the full width — it closes the grid. */
}

/* A quiet line under the wall, in place of the carousels' two counters. */
.works-count {
    margin: clamp(26px, 3vw, 40px) 0 0;
    font-family: var(--sans); font-size: .62rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase; color: var(--ink-2);
}



/* The written introduction now closes the section rather than opening it:
   the works are shown first, then described. Centred, narrower than the body
   column and above a hairline, so it reads as a catalogue note rather than a
   second lede. Every card carries its own "Enquire", so there is deliberately
   no section-level enquiry link competing with them. */
.collection-intro.collection-note {
    max-width: 64ch;
    /* LEFT, not centred, and on the left axis rather than in the middle of the
       band. This was the only centred long-form text on the page, and a centred
       block under a left-aligned grid reads as a different website — the eye has
       to find a new starting edge for four lines and then give it up again. Its
       hairline is the width of the note, the same way the press strip's and the
       film's wall label are — a rule belongs to its column, not to the band. */
    margin: clamp(40px, 4.5vw, 40px) 0 0;
    padding-top: clamp(28px, 3.2vw, 42px);
    border-top: 1px solid var(--line);
}
.collection-note h3 { margin-bottom: .8em; }
.collection-note p { margin-top: 1.1em; }
.collection-note p:first-of-type { margin-top: 0; }

/* ============================== WHY BANKSY ============================== */
.why { background: var(--paper-2); }
.why-head { max-width: 64ch; }
.why-head .eyebrow { color: var(--ink-2); }
.why-head h2 { margin: .55em 0 .7em; }
.why-lede { color: var(--ink-2); max-width: 58ch; }

.why-grid {
    list-style: none; margin: clamp(44px, 5vw, 78px) 0 0; padding: 0;
    display: grid; grid-template-columns: 1fr; gap: clamp(32px, 3.6vw, 56px);
}
.why-item { padding-top: 22px; border-top: 1px solid var(--line-2); }
/* The numeral is the ornament — set in the display serif, large and pale, it
   does the work a rule or an icon would do without adding either. */
.why-num {
    font-family: var(--serif); font-size: 1.5rem; line-height: 1;
    color: var(--gilt-text); margin: 0 0 .7em;
}
.why-item h3 { font-size: clamp(1.2rem, 1.9vw, 1.5rem); margin-bottom: .6em; }
.why-item p { color: var(--ink-2); margin: 0; }
.why-item em { font-style: italic; }
/* The closing note and its link are two things, not one sentence. Inline, the
   link sat tight against the last word and picked up the paragraph's ragged edge;
   on its own line, aligned to the column, it reads as the next step. */
.why-note {
    margin: clamp(46px, 5vw, 80px) 0 0; max-width: 62ch;
    padding-top: clamp(26px, 3vw, 40px);
    border-top: 1px solid var(--line-2);
    color: var(--ink-2);
}
.why-note p { margin: 0; }
.why-note .link-arrow { display: inline-block; margin-top: clamp(26px, 2.8vw, 38px); }

/* ============================== PRESS ==============================
   Names set as type, not logotypes: the outlets are already named in the copy
   above, and this avoids reproducing six mastheads nobody has licensed. */
.press {
    margin-top: clamp(18px, 2.4vw, 26px);
    padding-top: clamp(18px, 2.4vw, 26px);
    /* --line-2, because the film sits on a tonal band: the paper hairline is
       invisible against --paper-2. House rule, and the reason the token exists. */
    border-top: 1px solid var(--line-2);
}
.press-label {
    margin: 0 0 18px;
    font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase; color: var(--ink-2);
}
.press-list {
    /* A two-column credits block, not a run-on line. The strip used to be a flex
       row with brass middots between the names, which worked when it spanned the
       full width of the Expertise section. In the film's copy column it does not:
       six names cannot hold one line, and a wrapped separator lands at the START
       of a row — a stray middot hanging in the gutter, reading as a bullet list
       someone broke. Two columns and no separators, at every width. */
    list-style: none; margin: 0; padding: 0;
    display: grid; grid-template-columns: 1fr 1fr;
    gap: 12px clamp(16px, 2.4vw, 30px);
}
.press-list li {
    font-family: var(--serif);
    font-size: clamp(1rem, 1.25vw, 1.16rem);
    line-height: 1.35;
    color: var(--ink);
}

/* ============================== TESTIMONIALS ============================== */
/* Renders only when config has entries — see $cfg['testimonials'].
   --paper-3, not --paper-2: it sits between the FAQ (white) and Enquire (paper-2),
   and on paper-2 it ran straight into the form band. verify-sections.js caught it. */
.testimonials { background: var(--paper-3); }
.testimonials-head { max-width: 60ch; margin-bottom: clamp(44px, 5vw, 76px); }
.testimonials-head .eyebrow { color: var(--ink-2); }
.testimonials-head h2 { margin-top: .55em; }

.testimonial-scroller { position: relative; }
/* A snap track, not a grid. Six quotes side by side were a wall nobody reads; one
   quote given the whole column is read. Native scrolling means it swipes on a
   phone, works with the keyboard (the track is focusable) and needs no JS at all —
   the arrows and the counter in main.js §0h are an enhancement on top. */
.testimonial-list {
    list-style: none; margin: 0; padding: 0 0 4px;
    display: flex; gap: clamp(28px, 3vw, 48px);
    overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.testimonial-list::-webkit-scrollbar { display: none; }
.testimonial-list:focus-visible { outline: 2px solid var(--gilt-text); outline-offset: 6px; }
/* Draggable by hand on a mouse or a pen — the cursor is the only affordance that
   says so, and without it nobody tries. Touch keeps the browser's own scrolling. */
@media (hover: hover) and (pointer: fine) {
    .testimonial-list { cursor: grab; }
    .testimonial-list.is-dragging { cursor: grabbing; user-select: none; }
    /* While the hand is on it, nothing underneath should highlight blue. */
    .testimonial-list.is-dragging * { user-select: none; }
}
.testimonial {
    flex: 0 0 100%; scroll-snap-align: start;
    padding-top: 24px; border-top: 1px solid var(--line-2);
    min-height: 190px;                 /* so the arrows do not jump between quotes */
}
.testimonial-controls {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    margin-top: clamp(26px, 3vw, 40px);
}
/* The round arrows came back with the testimonial scroller — they were deleted
   with the works carousels. Same brass-bloom as before, so the one pair of arrows
   left on the page still speaks the house language. */
.carousel-navs { display: flex; gap: 8px; }
.carousel-nav {
    width: 44px; height: 44px; border-radius: 50%;
    border: 1px solid var(--line-2); background: transparent;
    color: var(--ink); font-size: 1rem; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    position: relative; isolation: isolate; overflow: hidden;
    transition: color .4s var(--ease-lux), border-color .4s var(--ease-lux);
}
.carousel-nav::before {
    content: ""; position: absolute; inset: -1px; z-index: -1; border-radius: 50%;
    background: var(--gilt);
    transform: scale(0); transition: transform .5s var(--ease-lux);
}
.carousel-nav:hover::before { transform: scale(1); }
.carousel-nav:hover { border-color: var(--gilt); color: var(--paper); }

.testimonial-count {
    margin: 0;
    font-family: var(--sans); font-size: .62rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase; color: var(--ink-2);
    font-variant-numeric: tabular-nums;
}

/* The rating is one row of stars with a brass overlay clipped to a percentage, so
   a 3.5 renders as three and a half rather than being rounded up to a flattering
   four. Decorative — the aria-label on the row carries the number. */
.testimonial-stars { margin: 0 0 1em; line-height: 1; }
.stars-track {
    position: relative; display: inline-block;
    font-size: .78rem; letter-spacing: .22em;
    color: var(--line-2); white-space: nowrap;
}
.stars-fill {
    position: absolute; top: 0; left: 0;
    overflow: hidden; white-space: nowrap;
    color: var(--gilt-bright);
}
.testimonial blockquote { margin: 0; }
/* The quotation is set in the display serif at reading size — the only place on
   the page where someone else is speaking, so it should not look like body copy.
   No decorative quote marks: the indent and the attribution do that work. */
/* One quote, full column — it can take display size now. */
.testimonial blockquote p {
    margin: 0; max-width: 46ch;
    font-family: var(--serif); font-size: clamp(1.3rem, 2.4vw, 1.95rem);
    line-height: 1.42; color: var(--ink);
}
.testimonial-by {
    margin-top: 1.4em;
    display: flex; flex-direction: column; gap: 4px;
    font-family: var(--sans);
}
.testimonial-name {
    font-size: .68rem; font-weight: 500; letter-spacing: .18em; text-transform: uppercase;
    color: var(--ink);
}
.testimonial-detail { font-size: .78rem; color: var(--ink-2); }

/* ============================== SOURCING (tonal band) ============================== */
.sourcing { background: var(--paper-3); }
.sourcing-inner > * { max-width: 62ch; }   /* NB: .sourcing-inner is the .wrap itself —
                                              setting max-width on it would centre the
                                              whole band off the page's left axis. */
.sourcing p { color: var(--ink-2); margin-top: 1.2em; }
.sourcing .btn { margin-top: 2.2rem; }

/* ============================== SPOTLIGHT ============================== */
.spotlight { background: var(--paper-2); }
.spotlight-grid { display: grid; grid-template-columns: 1fr; gap: clamp(36px, 4.5vw, 72px); align-items: center; }
.spotlight-copy p { color: var(--ink-2); }
.spotlight-copy .eyebrow { color: var(--ink-2); }
.spotlight-meta { font-size: .82rem; letter-spacing: .04em; color: var(--ink-2); margin-bottom: 1.6em; }
.spotlight-media img { width: 100%; }

/* ============================== EXPERTISE ============================== */
/* Centred, not top-aligned: the copy column runs much longer than the image, and
   top-alignment left a large void beneath the photograph. */
/* STRETCH, not centre. On a 1000-1100px desktop the copy column runs to ~650px
   while the photograph was only ~330px, so the image floated in the middle of a
   tall empty column. Stretched, it becomes a full-height panel beside the text —
   which is what the section was always trying to be. */
.expertise-top { display: grid; grid-template-columns: 1fr; gap: clamp(36px, 4.5vw, 72px); align-items: stretch; }
.expertise-copy p { color: var(--ink-2); }
.expertise-copy h2 { margin-bottom: .7em; }
/* Two photographs in the same frame, the second fading over the first and back on
   a slow loop. Absolutely positioned so the frame's height comes from the grid row
   (the copy column) rather than from either image. */
/* Stacked, the frame needs a shape of its own; side by side it takes the row's
   height instead — with the ratio left in place it would compute a width from that
   height and burst out of its column. */
.expertise-media { position: relative; overflow: hidden; aspect-ratio: 4 / 3; }
.expertise-media img {
    position: absolute; inset: 0;
    width: 100%; height: 100%; object-fit: cover;
}
/* Only the top image animates: the one beneath is always there, so there is no
   moment where the frame is empty. 18s is deliberately slow — a gallery, not a
   carousel. The global prefers-reduced-motion rule collapses the duration, which
   leaves the first photograph showing and nothing moving. */
.expertise-alt { animation: expertise-fade 18s var(--ease) infinite; }
@keyframes expertise-fade {
    0%, 6%    { opacity: 0; }
    44%, 56%  { opacity: 1; }
    94%, 100% { opacity: 0; }
}

/* Credentials as structure, not a sentence. Hairline-separated, serif numerals. */
.expertise-stats {
    /* Single column on the narrowest phones — two columns clipped the letterspaced
       labels ("Banksy Specialists Since") at 320px. */
    display: grid; grid-template-columns: 1fr;
    gap: 1px; background: var(--line);
    border: 1px solid var(--line);
    margin-top: clamp(48px, 5.5vw, 84px);
}
.stat { background: var(--paper); padding: clamp(24px, 3vw, 38px); display: flex; flex-direction: column; gap: .5em; }
.stat-label {
    font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase; color: var(--ink-2);
}
.stat-value { font-family: var(--serif); font-size: clamp(2rem, 3.4vw, 2.9rem); line-height: 1; }
.stat-value.stat-sm { font-size: clamp(1.05rem, 1.6vw, 1.3rem); line-height: 1.25; }

/* ============================== AUTHENTICATION ============================== */
.authentication { background: var(--paper-2); }
.authentication p { color: var(--ink-2); max-width: 68ch; }
.authentication h2 { margin-bottom: .8em; }

/* The verification process, rendered as a catalogue of steps rather than buried
   in a sentence. Content is unchanged — only its presentation. */
.auth-grid {
    list-style: none; margin: clamp(40px, 4.5vw, 66px) 0 clamp(32px, 3.5vw, 48px); padding: 0;
    display: grid; grid-template-columns: 1fr; gap: 1px;
    background: var(--line-2); border: 1px solid var(--line-2);
}
.auth-item { background: var(--paper); padding: clamp(22px, 2.8vw, 34px); }
.auth-num {
    display: block; font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .2em; color: var(--ink-2); margin-bottom: 1.1em;
}
.auth-item h3 { font-size: 1.25rem; margin-bottom: .45em; }
.auth-item p { font-size: .88rem; color: var(--ink-2); margin: 0; }

/* ============================== LEGACY / TIMELINE ============================== */
.legacy-intro { color: var(--ink-2); max-width: 72ch; }

/* Legacy and FAQ both sit on white, so without a rule between them the two
   sections read as one very long block. A hairline is enough — a tonal band
   here would fight the Enquire band directly below. */
.faq { border-top: 1px solid var(--line); }
/* The page has ONE left axis. `.centered` is neutralised on section headings so
   Legacy and FAQ line up with Collection, Authentication and Expertise; centred
   headings above left-aligned content read as a template. Keep it this way. */
.section > .wrap > h2.centered { text-align: left; }
/* h2 has margin:0 by default, so a section heading followed directly by a
   paragraph had no breathing room at all. */
.legacy > .wrap > h2,
.faq > .wrap > h2 { margin-bottom: .7em; }

.timeline { list-style: none; margin: clamp(48px, 5.5vw, 84px) 0 0; padding: 0; }
.timeline-item {
    display: grid; grid-template-columns: auto 1fr; gap: 0 22px;
    align-items: baseline;
    padding: 20px 0; border-top: 1px solid var(--line);
}
.timeline-item:last-child { border-bottom: 1px solid var(--line); }
.timeline-dot { display: none; }
.timeline-year { font-family: var(--serif); font-size: 1.5rem; line-height: 1; min-width: 3.4em; }
.timeline-text { color: var(--ink-2); font-size: .94rem; }

/* THE SECOND SHOW IN A YEAR. The year is not set again (see the loop in
   index.php), and the rule above the row is dropped so the pair reads as one
   entry with two lines rather than as an orphaned line with a missing date.
   The padding closes up for the same reason — and because at sixteen rows the
   section is long enough that every 20px counts. */
.timeline-item.is-same-year { border-top-color: transparent; padding-top: 0; margin-top: -6px; }

/* Sixteen rows is a lot of vertical distance for what is essentially a list of
   dates, and the whole point of it is that a visitor takes in HOW MANY there are.
   Two columns from a comfortable width keeps the run visible in one glance while
   staying in date order down the first column and on down the second — which is
   how a catalogue's exhibition history is set, and the only column arrangement
   that does not scramble a chronology. */
@media (min-width: 900px) {
    .timeline {
        columns: 2; column-gap: clamp(40px, 4.5vw, 76px);
    }
    /* A row split across the column break is unreadable. */
    .timeline-item { break-inside: avoid; }
    /* The rules meet the column edge on both sides, so the two columns read as
       one continuous list rather than as two tables. */
    .timeline-item:last-child { border-bottom: 1px solid var(--line); }
}

/* ============================== FAQ ============================== */
.faq-list { margin-top: clamp(40px, 4.5vw, 66px); border-top: 1px solid var(--line); }
.faq-item { border-bottom: 1px solid var(--line); }
.faq-item summary {
    list-style: none; cursor: pointer;
    display: flex; align-items: flex-start; justify-content: space-between; gap: 24px;
    padding: 26px 0;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-q { font-family: var(--serif); font-size: clamp(1.15rem, 1.8vw, 1.42rem); line-height: 1.28; }
.faq-icon { position: relative; flex: 0 0 auto; width: 15px; height: 15px; margin-top: .45em; }
.faq-icon::before, .faq-icon::after {
    content: ""; position: absolute; inset: 0; margin: auto;
    background: var(--ink); transition: transform .4s var(--ease), opacity .3s;
}
.faq-icon::before { width: 15px; height: 1px; }
.faq-icon::after  { width: 1px; height: 15px; }
.faq-item[open] .faq-icon::after { transform: rotate(90deg); opacity: 0; }
/* The animated box carries NO padding — the <p> does. Padding on a
   height-animated box never collapses to nothing, so `height: 0` still measured
   28px and every open began and every close ended with a jump. Moving it one
   element in is the whole fix, and it is the arrangement the gallery site uses.
   scrollHeight still includes it, so the measurement stays correct: change the
   value freely, but keep it on the <p>.

   Height is transitioned in CSS and driven from main.js §0b by setting an
   explicit pixel height, rather than run through the Web Animations API. Same
   curve, far less machinery, and the browser owns the interpolation. */
.faq-a {
    overflow: hidden; max-width: 72ch;
    transition: height .48s cubic-bezier(.22, .61, .36, 1);
}
.faq-a p {
    margin: 0; padding-bottom: 28px; color: var(--ink-2);
    /* The box opening was the only thing that moved; the text was simply there
       the instant it had room, which made the animation read as a jump. It rises
       into place a beat behind the box instead, so the panel reads as one
       gesture. main.js keeps [open] set for the whole closing animation, so on
       the way out the text fades WITH the collapse rather than vanishing first. */
    opacity: 0; transform: translateY(-8px);
    transition: opacity .34s var(--ease), transform .44s cubic-bezier(.22, .61, .36, 1);
}
.faq-item[open] .faq-a p { opacity: 1; transform: none; transition-delay: .1s; }
@media (prefers-reduced-motion: reduce) {
    .faq-a, .faq-a p { transition: none; }
}
.faq-cta { margin-top: clamp(32px, 3.5vw, 48px); color: var(--ink-2); font-size: .94rem; }

.faq-cta a { color: var(--ink); text-underline-offset: 3px; }

/* FURTHER READING. The second exit, one step quieter than the first: it is for a
   visitor who is not converting today, and it must not compete with the one who
   is. Hence its position — below .faq-cta, never above — a hairline to mark it as
   a different kind of thing, and no button anywhere in it.

   Ten links in one column reads as a queue and looks like a sitemap. Two columns
   with the label above its note reads as a shelf, which is what a reading list
   should look like, and it halves the height. The label carries the brass
   underline the rest of the page uses for a live link; the note is one step back
   in size and colour so the eye can run down the labels alone. */
.faq-reading {
    margin-top: clamp(30px, 3.2vw, 44px);
    padding-top: clamp(22px, 2.4vw, 30px);
    border-top: 1px solid var(--line);
}
.faq-reading-head {
    margin: 0 0 1.1em;
    font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase;
    color: var(--gilt-text);
}
.reading-list {
    list-style: none; margin: 0; padding: 0;
    display: grid; grid-template-columns: 1fr;
    gap: clamp(14px, 1.6vw, 20px) clamp(28px, 3.5vw, 56px);
}
@media (min-width: 760px) {
    .reading-list { grid-template-columns: 1fr 1fr; }
}
.reading-item { display: flex; flex-direction: column; gap: 3px; }
.reading-item a {
    font-size: .92rem; line-height: 1.4; color: var(--ink);
    text-decoration: underline;
    text-decoration-color: var(--gilt);
    text-underline-offset: .22em;
    text-decoration-thickness: 1px;
}
.reading-item a:hover { color: var(--gilt-deep); text-decoration-color: var(--gilt-deep); }
.reading-note {
    font-family: var(--sans); font-size: .74rem; line-height: 1.45; color: var(--ink-2);
}

/* ============================== ENQUIRE ============================== */
.enquire { background: var(--paper-2); }
.enquire-grid { display: grid; grid-template-columns: 1fr; gap: clamp(44px, 5vw, 80px); align-items: start; }
.enquire-copy .eyebrow { color: var(--ink-2); }
.enquire-copy h2 { margin-bottom: .75em; }
.enquire-copy p { color: var(--ink-2); }

.contact-list { list-style: none; margin: clamp(32px, 3.5vw, 48px) 0 0; padding: 0; }
.contact-list li { border-top: 1px solid var(--line-2); }
.contact-list li:last-child { border-bottom: 1px solid var(--line-2); }
.contact-list a {
    display: block; padding: 16px 0;
    color: var(--ink); text-decoration: none; font-size: .95rem;
    transition: color .3s var(--ease);
}
.contact-list a:hover { color: var(--gilt-text); }

/* Gallery hours. Deliberately the same hairline language as the contact list
   directly above it — a visitor reads the two as one block of practical
   information, not as two components. Rendered from $cfg['hours']; the current
   day is marked so nobody has to count rows to find it. */
/* --- open / closed ------------------------------------------------------
   A live answer above the timetable. Someone deciding whether to walk to
   Knightsbridge wants the state, not the schedule. */
.hours-head {
    display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between;
    gap: 10px 20px; margin-bottom: 14px;
}
.gallery-status {
    display: inline-flex; align-items: baseline; gap: 8px;
    margin: 0;
    font-family: var(--sans); font-size: .68rem; font-weight: 500;
    letter-spacing: .1em; color: var(--ink-2);
}
/* A dot, not a coloured word: the label must stay legible for anyone who cannot
   separate the two states by hue, so the WORDS carry the meaning and the dot only
   reinforces it. */
.status-dot {
    width: 7px; height: 7px; border-radius: 50%;
    /* Closed is the brand red, open is green. The red is --accent, sampled from
       the logo, so the closed state is the house's own colour rather than a
       generic warning red — this is "we are shut until Tuesday", not an error.
       The WORDS still carry the meaning either way: the dot only reinforces
       them, which is what keeps the pair legible to anyone who cannot separate
       the two by hue. */
    background: var(--accent); flex: 0 0 auto;
    transform: translateY(-1px);
}
.gallery-status.is-open { color: var(--ink); }
.gallery-status.is-open .status-dot { background: #2f6b3f; }   /* 5.1:1 on the band */

.hours { margin-top: clamp(30px, 3.2vw, 44px); }
.hours-title {
    font-family: var(--sans);
    font-size: .64rem; font-weight: 500; letter-spacing: .18em; text-transform: uppercase;
    color: var(--ink-2);
    margin: 0;
}
.hours-list { margin: 0; }
/* Days that share hours are collapsed into one line in index.php, so this is
   three rows, not seven — which is what lets them breathe. */
.hours-row {
    display: flex; align-items: baseline; justify-content: space-between; gap: 16px;
    padding: 15px 0;
    border-top: 1px solid var(--line-2);
    font-size: .95rem;
}
.hours-row:last-child { border-bottom: 1px solid var(--line-2); }
.hours-row dt { display: flex; align-items: baseline; gap: 12px; color: var(--ink); }
/* tabular-nums so 9.30 and 11 line up down the right edge like a printed card. */
.hours-row dd { margin: 0; color: var(--ink-2); text-align: right; font-variant-numeric: tabular-nums; }
/* "Closed" in the display serif, italic — the gallery's own card sets it that
   way, and it reads as a considered note rather than missing data. No opacity:
   dimming it would drop it below AA, and it is the one line someone about to
   cross London most needs to be able to read. */
.hours-row.is-closed dd em { font-family: var(--serif); font-size: 1.05rem; }
/* Brass letterspaced caps, not a filled chip: with three rows a solid block was
   the loudest thing in the column. 5.25:1 on the band — measured, see
   verify-contrast.js. */
.hours-today {
    font-family: var(--sans);
    font-size: .58rem; font-weight: 500; letter-spacing: .18em; text-transform: uppercase;
    color: var(--gilt-text);
}

/* --- How an acquisition works -----------------------------------------------
   A definition list, not a grid of cards: these are four answers to four
   questions, and a card each would give them the visual weight of products. On a
   hairline stack they read the way the specs in the lightbox read, which is the
   register the rest of the page already uses for facts.

   Deliberately quiet. It sits beside the form and its job is to remove an
   objection, not to compete for the eye with the thing it is standing next to. */
.acquisition {
    margin: clamp(34px, 4vh, 52px) 0 0;
    padding-top: clamp(22px, 2.6vw, 30px);
    border-top: 1px solid var(--line-2);
    max-width: 46ch;
}
.acq-item { padding: 14px 0; border-bottom: 1px solid var(--line-2); }
.acq-item:last-child { border-bottom: 0; padding-bottom: 0; }
.acq-item dt {
    font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--gilt-text);        /* brass: these are services, not small print */
    margin-bottom: .5em;
}
.acq-item dd { margin: 0; font-size: .86rem; line-height: 1.55; color: var(--ink-2); }

/* --- WhatsApp ---------------------------------------------------------------
   Three places now, all gated on the same built link ($waLink in index.php,
   empty when $cfg['whatsapp'] is empty or unparseable). See config.php. The
   footer link needs no rule of its own — .footer-col is a flex column with its
   own gap, and adding a margin here would space it differently from its
   neighbours. It is listed only so the next person looks for one and finds this.

   In the BAR it is the out-of-hours substitute for Call. In hours the phone is
   the better channel and this collapses exactly the way Call does when closed,
   so the row is always two buttons wide and never three. */
/* WITH ENQUIRE GONE the bar holds one button at a time, not two: the phone in
   hours, WhatsApp out of them. The collapse below is what swaps them — and it is
   still the right mechanism, because the two must never both show. What changed
   is that the survivor now takes the whole width instead of half of it. */
.mobile-cta:not(.is-closed) .mobile-whatsapp {
    flex-grow: 0; flex-basis: 0; opacity: 0; padding-inline: 0;
    pointer-events: none; overflow: hidden;
}
.mobile-cta.is-closed .mobile-whatsapp { border-color: var(--line-2); color: var(--ink-2); }

/* The third placement: under the enquiry form, for the visitor who has read the
   fields and would rather type one line. It sits OUTSIDE <form>, above the
   panel's bottom padding, separated by a hairline so it reads as a different
   route and not as more small print belonging to the button.

   Kept quiet on purpose. It is an alternative to the primary action, so it gets
   a text link and not a second button competing with Send Enquiry — and the
   brass underline is what marks it as a live channel rather than a footnote. */
.form-alt {
    margin: 22px 0 0; padding-top: 18px;
    border-top: 1px solid var(--line);
    font-family: var(--sans); font-size: .82rem; line-height: 1.5;
    color: var(--ink-2); text-align: center;
}
.form-alt a {
    color: var(--ink);
    text-decoration: underline;
    text-decoration-color: var(--gilt);
    text-underline-offset: .22em;
    text-decoration-thickness: 1px;
}
.form-alt a:hover { color: var(--gilt-deep); text-decoration-color: var(--gilt-deep); }
/* The number is the reassurance, not the instruction — one step back from the
   link, and never allowed to wrap mid-number. */
.form-alt-num { display: inline-block; white-space: nowrap; color: var(--ink-2); }
@media (max-width: 520px) {
    /* On a phone the line is long enough to break awkwardly between the words
       and the number, so give the number its own line deliberately. */
    .form-alt-num { display: block; margin-top: 4px; }
}

.enquire-form-wrap { background: var(--paper); color: var(--ink); padding: clamp(28px, 3.6vw, 54px); }

.field { margin-bottom: 20px; }
.field label,
.consent > span { font-family: var(--sans); }
.field label {
    display: block; margin-bottom: 9px;
    font-size: .64rem; font-weight: 500; letter-spacing: .18em; text-transform: uppercase;
    color: var(--ink-2);
}
.field input, .field textarea {
    width: 100%; font: inherit; font-size: .95rem; color: var(--ink);
    background: var(--paper-2);
    border: 1px solid var(--line);
    padding: 15px 16px;
    transition: border-color .25s var(--ease), background-color .25s var(--ease);
}
.field input:hover, .field textarea:hover { border-color: #c9c2b5; }
.field input:focus, .field textarea:focus {
    outline: none; border-color: var(--ink); background: #fff;
}
.field textarea { resize: none; min-height: 96px; overflow: hidden; }
.field input::placeholder, .field textarea::placeholder { color: #8d867c; }
.field-row { display: grid; grid-template-columns: 1fr; gap: 0 20px; }

.req { color: var(--accent); }
.opt { color: var(--ink-2); letter-spacing: .06em; text-transform: none; font-size: .68rem; }

.field-error { display: block; margin-top: 7px; font-size: .78rem; color: var(--accent); }
.field.has-error input, .field.has-error textarea { border-color: var(--accent); }

.consent { display: flex; align-items: flex-start; gap: 12px; margin: 6px 0 4px; font-size: .84rem; color: var(--ink-2); line-height: 1.5; cursor: pointer; }
.consent input { flex: 0 0 auto; width: 20px; height: 20px; margin: 2px 0 0; accent-color: var(--ink); }
.consent a { color: var(--ink); text-underline-offset: 3px; }
.consent-error { margin-bottom: 12px; }

.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-error {
    margin: 0 0 16px; padding: 13px 16px;
    background: var(--accent-tint);
    border-left: 2px solid var(--accent);
    color: var(--accent); font-size: .86rem;
}
.enquire-form .btn { margin-top: 10px; }
.form-fineprint { margin-top: 16px; font-size: .78rem; color: var(--ink-2); text-align: center; }

.form-success { display: none; }
.form-success.is-visible { display: block; }
.form-success h3 { font-size: 1.6rem; margin-bottom: .5em; }
.form-success p { color: var(--ink-2); }
.form-success a { color: var(--ink); }
.enquire-form.is-hidden { display: none; }

/* ============================== FOOTER ============================== */
.site-footer {
    background: var(--paper-3); color: var(--ink);
    padding: clamp(72px, 8vw, 128px) 0 0;
    /* A brass hairline closes the page. The border above the footer was --line-2,
       which just looked like another section join; this reads as the rule at the
       foot of a catalogue page. */
    border-top: 1px solid var(--frame);
}

/* Four columns on a desktop, stacked below. The brand column is given half again
   the width of the link columns so the wordmark and the address never cramp. */
.footer-grid {
    display: grid; grid-template-columns: 1fr;
    gap: clamp(44px, 4.5vw, 72px);
}
.footer-logo img { height: 48px; width: auto; }
/* The house line is set in the display serif, italic — one sentence of voice among
   four columns of navigation, and the only italic in the footer. */
.footer-line {
    margin: 26px 0 0; max-width: 30ch;
    font-family: var(--serif); font-style: italic;
    font-size: 1.12rem; line-height: 1.5; color: var(--ink-2);
}
.footer-address {
    margin: 22px 0 0; font-style: normal;
    font-size: .84rem; line-height: 1.7; color: var(--ink-2);
}

/* Each column title sits under a short brass rule — the detail that separates a
   catalogue colophon from a list of links. */
.footer-heading {
    position: relative;
    font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase;
    color: var(--ink-2);
    margin: 0 0 26px; padding-top: 18px;
}
.footer-heading::before {
    content: ""; position: absolute; top: 0; left: 0;
    width: 26px; height: 1px; background: var(--gilt-bright);
}
.footer-col { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; }
.footer-col a {
    font-family: var(--sans); font-size: .84rem;
    color: var(--ink); text-decoration: none;
    position: relative; padding-bottom: 2px;
    transition: color .45s var(--ease-lux);
}
/* The rule draws itself in rather than the label merely changing colour — the same
   hover language as the nav, so the footer feels like part of the same object. */
.footer-col a::after {
    content: ""; position: absolute; left: 0; bottom: 0; height: 1px; width: 100%;
    background: var(--gilt-bright);
    transform: scaleX(0); transform-origin: left;
    transition: transform .5s var(--ease-lux);
}
.footer-col a:hover { color: var(--gilt-text); }
.footer-col a:hover::after { transform: scaleX(1); }

.footer-tel {
    font-family: var(--serif) !important;
    font-size: 1.32rem !important; letter-spacing: .01em;
    font-variant-numeric: tabular-nums;
}
.footer-contact .footer-status { margin-top: 8px; }

/* --- the hours, and the door they belong to -------------------------------
   Brought down from the enquiry section: phone, email, hours and address are one
   piece of practical information and they were split across two places, three of
   them printed twice. Set tighter than the table in the contact block was — a
   footer is scanned, not read — with the two columns held apart by tabular
   numerals rather than by a rule, which would have added a fourth horizontal line
   to a footer that already has plenty. */
.footer-hours {
    margin: 16px 0 0; padding: 0;
    display: grid; gap: 5px;
    font-family: var(--sans); font-size: .74rem; line-height: 1.5;
}
.footer-hours-row {
    display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
}
.footer-hours dt { color: var(--ink-2); }
.footer-hours dd {
    margin: 0; color: var(--ink-2);
    font-variant-numeric: tabular-nums; white-space: nowrap;
}
.footer-hours dd em { font-style: italic; }
/* Today in ink rather than in brass: brass is the accent for things you can act
   on, and a row of a timetable is not one of them. */
.footer-hours-row.is-today dt,
.footer-hours-row.is-today dd { color: var(--ink); }

/* Social, at the foot of the brand column. Set as type with a brass middot
   between, the same treatment as the press strip and for the same reason: there
   is no icon set in this design, and two borrowed glyphs would be the only images
   on the page that are not artwork. Sized down from the link columns so they read
   as a footnote to the wordmark rather than a fifth column of navigation. */
.footer-social {
    list-style: none; margin: 20px 0 0; padding: 0;
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 14px;
}
.footer-social a {
    position: relative;
    font-family: var(--sans); font-size: .68rem; font-weight: 500;
    letter-spacing: .14em; text-transform: uppercase;
    color: var(--ink-2); text-decoration: none;
    transition: color .45s var(--ease-lux);
}
.footer-social a::after {
    content: ""; position: absolute; left: 0; bottom: -3px; height: 1px; width: 100%;
    background: var(--gilt-bright);
    transform: scaleX(0); transform-origin: left;
    transition: transform .5s var(--ease-lux);
}
.footer-social a:hover { color: var(--gilt-text); }
.footer-social a:hover::after { transform: scaleX(1); }
.footer-social li + li::before {
    content: "\00b7"; margin-right: 14px; color: var(--gilt-bright);
}

.footer-contact-address {
    margin: 16px 0 0;
    font-style: normal; font-size: .84rem; line-height: 1.6;
}
.footer-contact-address a { color: var(--ink-2); }

/* --- the private-list line ------------------------------------------------
   A full-width band between the columns and the small print, hairlines above and
   below. It is the only invitation in the footer, so it is given its own room
   rather than being buried in a column. */
.footer-private {
    margin-top: clamp(52px, 5.5vw, 84px);
    /* padding-BLOCK only: this element is also a .wrap, and a shorthand `padding`
       would wipe out its gutter — the band then runs to the screen edge on a phone
       and sits left of the columns on a desktop. */
    padding-block: clamp(26px, 3vw, 40px);
    border-top: 1px solid var(--line-2);
    display: grid; grid-template-columns: 1fr; gap: 18px; align-items: baseline;
}
.footer-private-copy {
    margin: 0; max-width: 62ch;
    font-family: var(--serif); font-size: 1.12rem; line-height: 1.55; color: var(--ink);
}

/* A hairline, then the small print: legal, the claims worth repeating, and a quiet
   way back to the top. */
.footer-base {
    padding-block: 26px 34px;      /* not `padding`, for the reason above */
    border-top: 1px solid var(--line-2);
    display: flex; flex-direction: column; align-items: center; gap: 12px;
    text-align: center;
}
.footer-legal,
.footer-assurance {
    margin: 0;
    font-family: var(--sans); font-size: .64rem; font-weight: 500;
    letter-spacing: .14em; color: var(--ink-2);
}
.footer-assurance { text-transform: uppercase; letter-spacing: .17em; }
.footer-top {
    font-family: var(--sans); font-size: .64rem; font-weight: 500;
    letter-spacing: .17em; text-transform: uppercase;
    color: var(--ink-2); text-decoration: none;
    transition: color .45s var(--ease-lux);
}
.footer-top:hover { color: var(--gilt-text); }

/* ============================== STICKY MOBILE CTA ============================== */
.mobile-cta {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 90;
    /* Flex, not grid: a button that is no longer wanted collapses its flex-grow to
       zero and the other one GROWS INTO the space, left to right, instead of the
       row snapping to a new column count. */
    /* Paper, not near-black. Rule 1 of this design system is that the page is
       LIGHT and separation comes from tonal steps and hairlines — the dark slab was
       the one place that broke it, and on a phone it is the element on screen the
       most. Alabaster with a hairline, a brass primary and an ink outline reads as
       part of the page instead of a toolbar bolted to the bottom of it. */
    display: block;
    /* Nearly solid. At .93 the page's own text read through the bar and collided
       with the status line — and translucency is not how this design system
       separates things anyway: tonal steps and hairlines are. The blur stays for
       the browsers that have it, but nothing depends on it. */
    background: rgba(251, 250, 247, .975);
    -webkit-backdrop-filter: saturate(140%) blur(10px); backdrop-filter: saturate(140%) blur(10px);
    border-top: 1px solid var(--frame);          /* the same brass hairline as the footer */
    padding: 9px var(--gutter);
    padding-bottom: calc(9px + env(safe-area-inset-bottom, 0px));
    transition: transform .55s var(--ease-lux), opacity .35s var(--ease-lux);
}
/* The bar does not meet the visitor at the door.
   Arriving from an ad to a page that already asks you to enquire, before you have
   seen a single work, is the digital equivalent of being approached the moment you
   step through the door — the one thing a Knightsbridge townhouse does not do. It
   waits until the hero's own "See The Collection" has scrolled away, which is the
   principled rule as well as the polite one: the bar exists to REPLACE a call to
   action that has left the screen, not to add a second one beside it.

   `cta-armed` is set by main.js (§0g2), so with JavaScript off the bar behaves as
   it always did rather than being hidden for good — the same rule the scroll
   reveals follow. visibility, not just opacity, so the hidden bar is out of the
   tab order and out of a screen reader's way; its transition is delayed by the
   length of the slide so it does not blink out mid-animation. */
/* On the first paint there is nothing to animate FROM, and the visibility
   transition below is delayed by the length of the slide — so for half a second
   after load the bar was `visibility: visible` while sitting off screen at zero
   opacity. Harmless to look at, wrong to leave: it kept the bar in the tab order
   for that half second, and it made the mobile suite flake, which is worse than
   the bug. main.js drops .cta-init on the second frame. */
body.cta-init .mobile-cta { transition: none !important; }
body.cta-armed .mobile-cta {
    /* 100% of a 63px rail plus a margin, rather than a percentage alone: the bar
       got shorter when it became a single line, and a percentage that was generous
       against a 104px slab is a thinner cushion against this one. */
    transform: translateY(calc(100% + 16px)); opacity: 0; visibility: hidden; pointer-events: none;
    transition: transform .55s var(--ease-lux), opacity .3s var(--ease-lux), visibility 0s linear .55s;
}
body.cta-armed.cta-ready .mobile-cta {
    transform: none; opacity: 1; visibility: visible; pointer-events: auto;
    transition: transform .55s var(--ease-lux), opacity .35s var(--ease-lux), visibility 0s;
}
/* ONE LINE, not a slab.
   It used to be 104px tall on a 390px phone — a status line above two full-width
   blocks — which is an eighth of the screen and reads as an app toolbar bolted to
   the page rather than part of it. It is now a single rail: where we are on the
   left, what you can do on the right, at 64px including the home indicator.
   Two full-width buttons shout; a line of small caps and two compact actions is
   the same offer made quietly, which is the register the rest of the page is in. */
.mobile-cta-row {
    display: flex; align-items: center; gap: 10px;
    transition: gap .7s var(--ease-lux);
}
/* The live open/closed line, now the left-hand end of the rail rather than a line
   above it. It takes the slack, so the two actions sit against the right edge. */
.cta-status {
    flex: 1 1 auto; min-width: 0;
    justify-content: flex-start;
    margin: 0;
    font-size: .58rem; letter-spacing: .1em;
}
.cta-status .status-label {
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Two equally-boxed actions read as a toolbar. Call keeps its box — a 55+ audience
   should never have to guess what is tappable — but it recedes to the palest
   hairline and secondary ink, so the brass is unmistakably the offer and Call is
   the quiet alternative beside it. */
.mobile-cta .mobile-call { border-color: var(--line-2); color: var(--ink-2); }
.mobile-cta .btn {
    flex: 0 0 auto; min-width: 0; min-height: 44px;   /* 44px is the tap target floor */
    padding: 12px 18px; font-size: .58rem; letter-spacing: .12em;
    white-space: nowrap;
    transition: flex-grow .7s var(--ease-lux), opacity .4s var(--ease-lux),
                padding .7s var(--ease-lux), background-color .5s var(--ease-lux),
                border-color .5s var(--ease-lux), color .5s var(--ease-lux);
}
/* Under 360px the three of them cannot share a line honestly, so the status takes
   one of its own rather than being cut to an ellipsis that says nothing. */
@media (max-width: 359px) {
    .mobile-cta-row { flex-wrap: wrap; }
    .cta-status { flex: 1 0 100%; justify-content: center; margin-bottom: 8px; }
    .mobile-cta .btn { flex: 1 1 0; }
}

/* A button that has been withdrawn: no width, no ink, no target. */
.mobile-cta .btn.is-withdrawn {
    flex-grow: 0; flex-basis: 0;
    opacity: 0; padding-inline: 0;
    pointer-events: none; overflow: hidden;
}
.mobile-cta.is-closed .mobile-cta-row,
.mobile-cta.is-on-form .mobile-cta-row { gap: 0; }

/* Out of hours the Call button goes. A Call button that rings an empty gallery is
   worse than no Call button — the visitor taps it, nobody answers, and that is
   the impression they leave with. */
.mobile-cta.is-closed .mobile-call {
    flex-grow: 0; flex-basis: 0; opacity: 0; padding-inline: 0;
    pointer-events: none; overflow: hidden;
}
/* Standing at the form, Enquire is pointing at where you already are. It withdraws
   and Call sweeps across the bar — so the one thing still worth offering, a voice
   on the other end, is under the thumb while the visitor types. */
.mobile-cta.is-on-form .mobile-enquire {
    flex-grow: 0; flex-basis: 0; opacity: 0; padding-inline: 0;
    pointer-events: none; overflow: hidden;
}
/* Call alone should not read as a secondary outline; it is now the only action,
   and it spreads into the room Enquire has left. */
.mobile-cta.is-on-form:not(.is-closed) .mobile-call {
    flex-grow: 1;
    background: var(--gilt); border-color: var(--gilt); color: var(--paper);
}
.mobile-cta.is-on-form:not(.is-closed) .mobile-call::before { background: var(--gilt-deep); }
/* At the form, out of hours, the status line goes with the bar. */
.mobile-cta.is-closed.is-on-form .cta-status { display: none; }
/* At the form, out of hours, the bar has nothing left to say. It leaves rather
   than sitting there empty over the fields. */
/* Written to outrank the reveal rule above (`body.cta-armed.cta-ready`), which is
   three classes deep and would otherwise pin the bar open at the form out of
   hours. The two states say opposite things about the same bar; this one wins,
   because "there is nobody to call and you are already writing to us" is the more
   specific truth. */
.mobile-cta.is-closed.is-on-form,
body.cta-armed.cta-ready .mobile-cta.is-closed.is-on-form {
    transform: translateY(110%); opacity: 0; pointer-events: none;
}

/* ============================== ARTWORK LIGHTBOX ==============================
   A viewing room, not a modal: the work large on a mount, its catalogue details
   beside it, and prev/next so a collector can go through the set without
   closing and reopening. Navigation WRAPS — reaching the last work and only
   being able to go backwards is a dead end the visitor has to notice and undo.
   -------------------------------------------------------------------------- */
.lightbox {
    position: fixed; inset: 0; z-index: 200; display: none;
    align-items: center; justify-content: center;
    background: rgba(20, 17, 14, .93);
    -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
    padding: clamp(14px, 4vw, 52px);
}
.lightbox.is-open { display: flex; }
/* Clear band at the top so the close button never lands on the artwork. */
/* The overlay reserves room at the top for the close button, because the card is
   max-height:100% and centre-aligned: the moment it is tall enough to fill the
   overlay's content box, its top edge lands exactly on the padding edge — under
   the button.

   This used to be relaxed at `min-width: 1000px`, on the assumption that a
   desktop-WIDTH window always has the vertical room. It does not: at 1024x768 the
   padding fell to 41px (4vw) while the button occupies 16-64px, and adding the
   per-impression spec rows made the card tall enough to reach it. The close
   button landed on the card.

   Whether the card can reach the top is a question about HEIGHT, so the
   relaxation is now gated on height. 820px is the same threshold the short-window
   rules above already use. */
.lightbox { padding-top: 76px; }
@media (min-width: 1000px) and (min-height: 820px) {
    .lightbox { padding-top: clamp(14px, 4vw, 52px); }
}


.lightbox-inner {
    position: relative; background: var(--paper);
    /* 100% of the flex container's CONTENT box, so the card always respects the
       overlay's padding. With 92vh it could exceed the available height and, being
       centre-aligned, overflow BOTH ends — pushing its top under the close button. */
    width: 100%; max-width: 1140px; max-height: 100%;
    /* The CARD does not scroll. Only the details column does (below). When the
       whole card scrolled, the Enquire button scrolled away with it on anything
       short — which is the same conversion leak in a different disguise. */
    overflow: hidden;
    display: grid;
    grid-template-columns: 1fr;
    grid-template-rows: minmax(0, auto) minmax(0, 1fr);
}

/* --- media --- */
.lightbox-media {
    position: relative;
    background: var(--paper-2);
    display: flex; align-items: center; justify-content: center;
    /* Extra bottom padding reserves a clear band for the zoom hint — at even
       padding it sat on the frame's bottom edge. */
    padding: clamp(16px, 3vw, 44px) clamp(16px, 3vw, 44px) clamp(46px, 6vh, 70px);
    overflow: hidden;
    min-height: 0;
    /* Double-tap here is OUR zoom (main.js §0d), not the browser's. manipulation
       also removes the ~300ms click delay Safari holds while it waits to see
       whether a tap is the first of a pair. */
    touch-action: manipulation;
}
.lb-zoom {
    display: block; padding: 0; border: 0; background: none;
    cursor: zoom-in; line-height: 0;
    max-width: 100%;
}
.lb-zoom:hover + .lb-zoom-hint { border-color: var(--gilt-bright); color: var(--gilt-text); }
.lb-zoom img {
    /* Sized against the viewport height so a short window shrinks the work rather
       than squeezing the details out. 34vh leaves the specs a usable column on a
       640px phone; the desktop override below relaxes it. */
    max-width: 100%; max-height: 34vh; width: auto; height: auto;
    object-fit: contain;
    background: #fff;
    padding: clamp(8px, 1.2vw, 16px);
    border: 1px solid var(--frame);
    transition: transform .7s var(--ease-lux);
    transform-origin: center center;
}
/* There is deliberately no zoom state here any more. Magnifying the work inside
   this box cropped the picture in order to study it — the visitor lost the whole
   composition at exactly the moment they wanted more of it. The button now opens
   the full-screen viewer at the foot of this file instead. */

.lb-zoom-hint {
    position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
    font-family: var(--sans); font-size: .58rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase;
    color: var(--ink-2);
    background: var(--paper); border: 1px solid var(--line);
    padding: 7px 14px;
    transition: opacity .4s var(--ease-lux);
    pointer-events: none;
}

/* --- catalogue details --- */
.lightbox-info {
    padding: clamp(24px, 3vw, 46px);
    display: flex; flex-direction: column;
    /* The one scroll container. min-height:0 is required or a grid/flex child
       refuses to shrink below its content and scrolls the card instead. */
    min-height: 0;
    overflow-y: auto;
    /* Both ends belong to sticky elements now: .lb-browse carries the top padding
       and .lb-cta the bottom. See each of those rules for why the container
       cannot keep them. */
    padding-top: 0;
}
/* PINNED TO THE TOP, the way .lb-cta is pinned to the bottom. Between them the
   details column now has a fixed head and a fixed foot and only the catalogue
   entry moves, which is how a reference panel should behave.

   It matters more than it looks. This strip carries the two things a visitor
   needs while READING rather than before they start: which collection they are
   in — paging runs straight from signed into unsigned, works at very different
   prices — and the arrows to go on. Both used to scroll away at the top of a
   1,400px column, so by the time somebody had read to the end of an entry the
   only way onward was to scroll all the way back up to find the arrow, or to
   close the card. The story added in §5f made the column longer and made this
   worse.

   The top padding of .lightbox-info moves ONTO this element (see the rule below)
   for the same reason .lb-cta needed padding-bottom: 0 on the container: padding
   is inside the scrollport, so content scrolls up through it and would appear
   ABOVE a header stuck at top: 0. Zero the container's padding, put it on the
   sticky element, and the background covers the full width of the strip. */
.lb-browse {
    display: flex; align-items: center; justify-content: space-between; gap: 16px;
    position: sticky; top: 0; z-index: 2;
    background: var(--paper);
    padding-top: clamp(24px, 3vw, 46px);
    padding-bottom: 14px;
    margin-bottom: calc(1.2rem - 14px);
}
/* Mirror of .lb-cta::before: content fades out as it passes UNDER the header,
   rather than being cut off by a hard edge halfway through a line. */
.lb-browse::after {
    content: ""; position: absolute; left: 0; right: 0; top: 100%;
    height: 20px; pointer-events: none;
    background: linear-gradient(to bottom, var(--paper), rgba(251, 250, 247, 0));
}
.lb-navs { display: flex; gap: 8px; }
.lb-where {
    margin: 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 10px;
    font-family: var(--sans); font-size: .62rem; font-weight: 500;
    letter-spacing: .22em; text-transform: uppercase;
}
/* The collection name carries the brass, so a change of group is the thing the
   eye catches when paging across the boundary. */
.lb-collection { color: var(--gilt-text); }
.lb-collection:empty { display: none; }
.lb-collection + .lb-count::before { content: "·"; margin-right: 10px; opacity: .5; }
.lb-count { color: var(--ink-2); }
.lb-nav {
    width: 44px; height: 44px; border-radius: 50%;
    border: 1px solid var(--line-2); background: transparent;
    color: var(--ink); font-size: 1rem; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    position: relative; isolation: isolate; overflow: hidden;
    transition: color .45s var(--ease-lux), border-color .45s var(--ease-lux);
}
.lb-nav::before {
    content: ""; position: absolute; inset: -1px; z-index: -1; border-radius: 50%;
    background: var(--gilt);
    transform: scale(0); transition: transform .5s var(--ease-lux);
}
.lb-nav:hover::before { transform: scale(1); }
.lb-nav:hover { border-color: var(--gilt); color: var(--paper); }
/* A single work has nothing to page through. */
.lb-nav[hidden] { display: none; }
.lightbox-info h3 {
    font-family: var(--serif); font-style: italic;
    font-size: clamp(1.7rem, 3vw, 2.3rem); line-height: 1.1;
    margin: 0 0 1.4rem;
}

/* Label/value rows on hairlines — the vocabulary a collector actually reads. */
/* What the work is. Set in the serif at reading size — it is prose, and the specs
   below it are data; the page should not make a visitor read them in the same
   voice. Held to a measure so it stays two or three lines in the panel. */
.lb-desc {
    margin: 0 0 1.1em;
    font-size: .95rem; line-height: 1.6; color: var(--ink-2);
    max-width: 46ch;
}
/* ON A SMALL OR SHORT SCREEN THE FACTS COME FIRST. The info column is a scroll
   container, and on a 320x568 phone it is about 270px tall — which the picture,
   the counter and the title already fill. Adding the description pushed every
   spec row below the fold, so the panel showed a title and a button and nothing
   a collector could act on. Caught by screenshotting it rather than by reading
   the numbers, which said the button was visible and technically were right.

   Desktop keeps catalogue order — what it is, then the numbers. Here the order
   inverts: the numbers are what someone squinting at a phone actually needs, and
   the prose is enrichment that can wait until they scroll. DOM order is
   unchanged, so the reading order for a screen reader on a large screen is the
   considered one, and this is presentation only. */
@media (max-width: 619px), (max-height: 620px) {
    .lb-browse      { order: 0; }
    .lightbox-info h3 { order: 1; }
    .lb-specs       { order: 2; }
    .lb-impression  { order: 3; }
    .lb-desc        { order: 4; margin-top: 1.2em; margin-bottom: 0; }
    /* The story is enrichment on top of enrichment: last of the readable blocks,
       above only the pinned CTA. */
    .lb-story       { order: 5; }
    .lb-cta         { order: 6; }
}
/* NOT hidden on short screens, though I tried that first. Screenshotting a
   320x568 phone with and without it produced the identical panel: the counter,
   the title and the pinned Enquire button already fill the ~270px scroll area, so
   every specification was below the fold there LONG BEFORE the description
   existed. Hiding it would have been a fix for a problem it does not cause.
   The reorder above is the part that matters — the facts scroll into view first.
   The real issue on that viewport is older and separate; see DOCUMENTATION.md. */

.lb-specs { margin: 0 0 auto; padding: 0; border-top: 1px solid var(--line); }
.lb-spec {
    display: grid; grid-template-columns: 1fr; gap: 4px;
    padding: 12px 0; border-bottom: 1px solid var(--line);
    align-items: baseline;
}
@media (min-width: 620px) {
    .lb-spec { grid-template-columns: 8rem 1fr; gap: 0 18px; padding: 13px 0; }
}
.lb-spec dt {
    font-family: var(--sans); font-size: .58rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase;
    color: var(--ink-2);
}
.lb-spec dd { margin: 0; font-size: .92rem; color: var(--ink); }
.lb-spec[hidden] { display: none; }

/* THE IMPRESSION. A second list under the first, separated by air rather than by
   a heading: the rows read as a continuation of the catalogue entry, which is
   what they are, and a heading would announce "extra information" for a work that
   happens to have none. The whole list is hidden when a work carries no
   per-impression facts — an empty <dl> under a rule reads as missing data, and on
   a page whose argument is provenance that is worse than saying nothing.
   No top border: the list above already ends in one. */
.lb-impression { margin-top: clamp(18px, 2.4vh, 26px); border-top: 0; }
.lb-impression[hidden] { display: none; }

/* ON THE RECORD. The documented history of the image, under the two spec lists.
   It carries a label where the impression block deliberately does not, and the
   reason is the opposite of the one there: the impression rows continue the
   catalogue entry and a heading would interrupt it, whereas this is a change of
   register — from what we are selling to what is on the public record — and the
   reader should be told the register has changed.

   Set smaller and looser than .lb-desc, and one step back in colour. It is the
   third thing to read in the panel, and it should look like it. */
.lb-story {
    margin-top: clamp(18px, 2.4vh, 26px);
    padding-top: clamp(14px, 1.8vh, 20px);
    border-top: 1px solid var(--line);
    max-width: 52ch;
}
.lb-story[hidden] { display: none; }
.lb-story-label {
    margin: 0 0 .6em;
    font-family: var(--sans); font-size: .58rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase;
    color: var(--gilt-text);      /* brass: provenance, not small print */
}
.lb-story-text { margin: 0; font-size: .85rem; line-height: 1.7; color: var(--ink-2); }

/* The numbering note, under the edition figure rather than beside it — "Edition
   of 750" then, quieter, why an unsigned sheet says 750. Only two works have one. */
.lb-edition-note {
    display: block; margin-top: 3px;
    font-family: var(--sans); font-size: .68rem; line-height: 1.45;
    color: var(--ink-2);
}
.lb-edition-note[hidden] { display: none; }

/* Sold reads in the house red wherever it appears — the same colour the form uses
   for a required field and the film uses for its kicker. It is a state, not a
   decoration, and it should never be mistakable for a price. */
.lb-spec dd.is-sold { color: var(--accent); font-weight: 500; }

.lb-cta { margin-top: clamp(22px, 3vh, 34px); }
 /* No button above it any more, so the 14px that separated the two would leave
    the line floating in the middle of the sticky foot. */
.lb-hint { font-size: .74rem; color: var(--ink-2); margin: 0; text-align: center; }

/* Short phones: buy height back from the work and the title so at least one spec
   row shows beneath them. A details column that is entirely hidden behind the
   pinned CTA gives no hint that there is anything to scroll to. */
@media (max-height: 720px) and (max-width: 999px) {
    .lb-zoom img { max-height: 26vh; }
    .lightbox-info h3 { font-size: 1.5rem; margin-bottom: .9rem; }
    /* Short phones: the strip is pinned, so every pixel it takes is a pixel the
       catalogue entry never gets back. Trim its own padding, not just the margin. */
    .lb-browse { padding-top: 18px; padding-bottom: 10px; margin-bottom: 0; }
    .lightbox-media { padding-bottom: 40px; }
}

/* Pinned to the bottom of the details column at EVERY size. Where the details
   fit, `margin-top: auto` on .lb-specs has already pushed it down and sticky does
   nothing visible; where they don't, it stays put while the specs scroll behind.
   An Enquire button you have to go looking for is a conversion leak at any width —
   a small laptop is just as capable of hiding it as a phone. */
.lightbox-info { padding-bottom: 0; }
.lb-cta {
    position: sticky; bottom: 0; z-index: 2;
    background: var(--paper);
    border-top: 1px solid var(--line);
    padding: 14px 0 clamp(16px, 2.5vh, 24px);
    margin-top: clamp(18px, 2.5vh, 30px);
}
/* Content fades out as it passes under the pinned CTA — on the smallest phones
   the specs sit entirely behind it, and without this there is nothing to suggest
   the column scrolls at all. */
.lb-cta::before {
    content: ""; position: absolute; left: 0; right: 0; bottom: 100%;
    height: 24px; pointer-events: none;
    background: linear-gradient(to top, var(--paper), rgba(251, 250, 247, 0));
    transition: height .4s var(--ease-lux);
}

/* --- "SCROLL FOR MORE" --------------------------------------------------------
   The problem it solves: a button pinned across the bottom of a panel is the
   universal sign that the panel has ENDED. On every viewport short of a large
   desktop the story and half the specifications are below it, and a visitor who
   reads the description and sees Enquire has been told, by the layout, that they
   have seen everything. They have usually seen about half.

   main.js puts .has-more on .lightbox-info whenever there is content below the
   fold and takes it off at the bottom of the scroll, so this is never showing
   when there is nothing to show — an affordance that lies once is not read again.

   Three things move together when it appears: the cue fades in, the fade under
   the button deepens from 24px to 56px so text visibly continues underneath
   rather than stopping, and the chevron bobs. Any one of them alone is too quiet
   in this palette; the fade alone was what was there before, and it was missed. */
.lb-more {
    margin: 0 0 10px; padding: 0;
    display: flex; align-items: center; justify-content: center; gap: 8px;
    font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .18em; text-transform: uppercase;
    color: var(--gilt-text);
    /* Collapsed rather than display:none, so appearing does not jump the button
       under the reader's thumb mid-scroll. */
    max-height: 0; opacity: 0; overflow: hidden;
    transition: max-height .35s var(--ease-lux), opacity .35s var(--ease-lux),
                margin-bottom .35s var(--ease-lux);
    pointer-events: none;      /* it is a sign, not a control */
}
.lightbox-info.has-more .lb-more { max-height: 2em; opacity: 1; }
.lightbox-info.has-more .lb-cta::before { height: 56px; }

/* Drawn, not typed: an arrow glyph sits on the text baseline and at this size
   renders differently in every font on every platform. Two borders always give
   the same chevron. */
.lb-more-chev {
    width: 6px; height: 6px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    transform: translateY(-1px) rotate(45deg);
}
.lightbox-info.has-more .lb-more-chev { animation: lb-more-bob 2.8s var(--ease-lux) infinite; }
@keyframes lb-more-bob {
    0%, 62%, 100% { transform: translateY(-1px) rotate(45deg); }
    78%           { transform: translateY(2px)  rotate(45deg); }
}
/* Held still, but still shown: the words and the deepened fade do the work, and
   somebody who has asked for less motion has not asked for less information. */
@media (prefers-reduced-motion: reduce) {
    .lb-more { transition: none; }
    .lightbox-info.has-more .lb-more-chev { animation: none; }
    .lb-cta::before { transition: none; }
}

/* --- controls --- */
.lightbox-close {
    position: absolute; z-index: 3; top: 16px; right: 16px;
    width: 48px; height: 48px; border-radius: 50%;
    border: 1px solid rgba(242,239,233,.3);
    background: rgba(20,17,14,.55); color: var(--on-noir);
    font-size: 1.7rem; line-height: 1;
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    transition: background-color .45s var(--ease-lux), border-color .45s var(--ease-lux), color .45s var(--ease-lux);
}
.lightbox-close:hover { background: var(--gilt); border-color: var(--gilt); color: var(--paper); }

/* NB: this block lives HERE, after every base lightbox rule, and not up beside
   the .lightbox padding it belongs with. Its declarations have the same
   specificity as the ones they override — .lightbox-media's `padding` shorthand,
   .lb-zoom img's max-height — so the only thing deciding the winner is source
   order. Written in its logical place it lost silently: the numbers changed, the
   page did not, and the close button carried on sitting on the artwork. */
/* --- ON A PHONE IT IS A SHEET, NOT A CARD ---------------------------------
   Inset on a small screen, the card was a floating panel with a band of dark
   overlay above it and another below — perhaps 120px of a 780px screen spent on
   showing that there is a page behind, which the visitor already knows. The
   details column is the part that was starved: it is the one thing here that
   scrolls, and it was giving up its height to a margin.

   Full-bleed, it reads the way a viewing room should on a phone — the work, then
   the catalogue entry under it, nothing else in the frame. Desktop is unchanged:
   there the inset IS the design, because a card floating on a dark ground is how
   a lightbox says "this is one work, and the wall is still behind it".

   100dvh with a 100vh fallback in front of it. On iOS Safari 100vh is the tallest
   the viewport ever gets, so a sheet sized in vh sits with its foot — the Enquire
   button — behind the browser toolbar until the visitor scrolls the chrome away.
   dvh tracks the chrome as it hides. Both are declared: a browser that does not
   know dvh ignores the second line and keeps the first. */
@media (max-width: 999px) {
    .lightbox { padding: 0; }
    .lightbox-inner {
        max-width: none;
        height: 100vh;  height: 100dvh;
        max-height: 100vh;  max-height: 100dvh;
    }
    /* The button now lands ON the sheet rather than on the overlay above it, so it
       needs to hold its own against the plate instead of against the dark ground.
       It sits over the mount, never over the work — .lb-zoom img is capped well
       below it — and verify-lightbox.js asserts exactly that. */
    .lightbox-close {
        top: 10px; right: 10px; width: 44px; height: 44px; font-size: 1.5rem;
        background: rgba(20, 17, 14, .72);
        border-color: rgba(242, 239, 233, .45);
    }
    /* THE HOME INDICATOR. The sheet now runs to the physical bottom of the screen,
       which on an iPhone is where the system's own bar lives. Without this the
       Enquire button sits under it and the first tap does nothing — the worst
       possible control to lose to a rounded corner. */
    .lb-cta { padding-bottom: calc(clamp(16px, 2.5vh, 24px) + env(safe-area-inset-bottom, 0px)); }
    /* A CLEAR BAND FOR THE BUTTON. Inset, the close sat on the dark overlay above
       the card and touched nothing. Full-bleed it sits on the mount — and at the
       plate's own 16px top padding it landed ON THE ARTWORK, which is the one
       thing on this page that may never be covered. 64px clears the 44px button
       at top:10px with room to spare; the sheet gained about 120px by losing its
       inset, so this is affordable and the work still reads larger than before. */
    .lightbox-media { padding-top: 64px; }

    /* THE HINT GOES INTO FLOW. On the desktop card it is absolutely positioned in
       a band the plate's bottom padding reserves for it, and that works because
       the plate is generous. On the sheet the 64px band above pushes the centred
       work down, and on a 320x568 SE — and in landscape — the picture's bottom
       edge met the hint. Reserving more padding just takes height from the details
       column, which is the thing the sheet was widened to feed.

       Stacked, an overlap is not something that can happen: the pane is a column,
       the work is one item and the hint is the next, and the browser does the
       arithmetic. verify-lightbox.js asserts it at six sizes. */
    .lightbox-media { flex-direction: column; gap: 12px; }
    .lb-zoom-hint {
        position: static; transform: none; left: auto; bottom: auto;
        flex: 0 0 auto;
    }
    /* …and the band that was reserved for it is no longer needed. */
    .lightbox-media { padding-bottom: clamp(16px, 3vw, 44px); }
}

/* The work gets more room on a TALL phone only. dvh, not vh, for the same reason
   the sheet is: on iOS a work sized in vh is sized against the tallest the
   viewport ever gets, so it overflows its pane while the browser chrome shows.

   min-height: 721px is not decoration — it is the mirror of the `max-height: 720px`
   short-screen rule further up, which caps the work at 26vh so the specs keep a
   usable column. Written without it, this sat LATER in the file and quietly
   overrode that cap: on a 320x568 SE and in landscape the work grew past the band
   the "tap to enlarge" hint sits in, and the hint landed on the picture. */
@media (max-width: 999px) and (min-height: 721px) {
    .lb-zoom img { max-height: 38dvh; }
}

/* =========================== FULL-SCREEN VIEWER ===========================
   The lightbox is the catalogue entry. This is the WORK — one screen per edition,
   nothing else on it but the controls, sitting ABOVE the lightbox so that closing
   it returns the visitor to the details of the same work.

   Why it exists: the old zoom scaled the image inside the lightbox's media box,
   which has `overflow: hidden`. Going closer therefore meant seeing LESS of the
   work — the visitor lost the composition at the exact moment they asked for more
   of it. Here the picture has the whole screen before any zoom happens at all.

   Three rules hold this together, and all three are load-bearing:
     1. .viewer-stage has `touch-action: none`. Every gesture — swipe between
        works, pinch, one-finger pan — is handled in main.js §0d2. Leave the
        browser any of them and it fights the viewer for the same finger.
     2. The TRACK translates for paging; the IMG transforms for zoom. Two
        transforms on two elements, never one element doing both, or the maths for
        "keep the point under the finger" has to unpick the page offset first.
     3. Nothing here animates during a gesture. Transitions are added by the
        `is-gliding` class only when the code, not the finger, is moving things.
   -------------------------------------------------------------------------- */
.viewer {
    position: fixed; inset: 0; z-index: 300; display: none;
    background: #14120f;
}
.viewer.is-open { display: block; }
.viewer-stage {
    position: absolute; inset: 0; overflow: hidden;
    touch-action: none;              /* see rule 1 above */
    cursor: zoom-in;
}
.viewer.is-zoomed .viewer-stage { cursor: grab; }
.viewer.is-panning .viewer-stage { cursor: grabbing; }
.viewer-track { display: flex; height: 100%; will-change: transform; }
.viewer-track.is-gliding { transition: transform .45s var(--ease-lux); }
.viewer-slide {
    flex: 0 0 100%; height: 100%;
    /* Grid with an explicit minmax(0,1fr) row, not place-items alone: WebKit will
       not resolve `max-height: 100%` on the image against an implicit row. This is
       the same trap documented for the collection plates. */
    display: grid; grid-template-rows: minmax(0, 1fr); place-items: center;
    padding: clamp(52px, 8vh, 88px) clamp(10px, 3vw, 64px) clamp(84px, 12vh, 116px);
    overflow: hidden;
}
.viewer-slide img {
    max-width: 100%; max-height: 100%; width: auto; height: auto;
    object-fit: contain;
    background: #fff;
    user-select: none; -webkit-user-select: none; -webkit-user-drag: none;
    transform-origin: center center;
    will-change: transform;
}
.viewer-slide img.is-gliding { transition: transform .4s var(--ease-lux); }

/* --- chrome ---
   Everything below sits over the work, so it is all a dark disc with a hairline:
   legible on a white print and on a black one, without a bar of solid colour
   eating into the picture. */
.viewer-close,
.viewer-arrow,
.film-modal-close {
    position: absolute; z-index: 3;
    width: 48px; height: 48px; border-radius: 50%;
    border: 1px solid rgba(242,239,233,.28);
    background: rgba(20,17,14,.55); color: var(--on-noir);
    line-height: 1; display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    transition: background-color .45s var(--ease-lux), border-color .45s var(--ease-lux), color .45s var(--ease-lux);
}
.viewer-close:hover,
.viewer-arrow:hover,
.film-modal-close:hover { background: var(--gilt); border-color: var(--gilt); color: var(--paper); }
.viewer-close,
.film-modal-close { top: 16px; right: 16px; font-size: 1.7rem; }
.viewer-arrow { top: 50%; margin-top: -24px; font-size: 1rem; }
.viewer-arrow-prev { left: clamp(10px, 2.5vw, 40px); }
.viewer-arrow-next { right: clamp(10px, 2.5vw, 40px); }
.viewer-arrow[hidden] { display: none; }

.viewer-bar {
    position: absolute; z-index: 3; left: 0; right: 0; bottom: 0;
    display: flex; align-items: center; justify-content: space-between; gap: 18px;
    padding: clamp(14px, 2.4vh, 22px) clamp(14px, 3vw, 32px);
    padding-bottom: calc(clamp(14px, 2.4vh, 22px) + env(safe-area-inset-bottom, 0px));
    /* A gradient, not a band: the work runs under it and stays visible. */
    background: linear-gradient(to top, rgba(20,17,14,.82), rgba(20,17,14,0));
    pointer-events: none;            /* only the controls take the pointer */
}
.viewer-bar > * { pointer-events: auto; }
.viewer-caption {
    margin: 0; min-width: 0;
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 12px;
}
.viewer-title {
    font-family: var(--serif); font-size: clamp(1.05rem, 2vw, 1.4rem);
    color: var(--on-noir); font-style: italic;
}
.viewer-count {
    font-family: var(--sans); font-size: .58rem; font-weight: 500;
    letter-spacing: .22em; text-transform: uppercase; color: var(--on-noir-2);
    font-variant-numeric: tabular-nums;
}
.viewer-zoom { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.viewer-zoom-btn {
    min-width: 44px; height: 44px; padding: 0 12px; border-radius: 999px;
    border: 1px solid rgba(242,239,233,.28);
    background: rgba(20,17,14,.55); color: var(--on-noir);
    font-family: var(--sans); font-size: .62rem; font-weight: 500;
    letter-spacing: .16em; text-transform: uppercase;
    display: inline-flex; align-items: center; justify-content: center;
    cursor: pointer;
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    transition: background-color .4s var(--ease-lux), border-color .4s var(--ease-lux), color .4s var(--ease-lux), opacity .3s var(--ease);
}
.viewer-zoom-btn:hover:not(:disabled) { background: var(--gilt); border-color: var(--gilt); color: var(--paper); }
.viewer-zoom-btn:disabled { opacity: .35; cursor: default; }
.viewer-zoom-btn:not(.viewer-fit) { font-size: 1.05rem; letter-spacing: 0; }

.viewer-hint {
    position: absolute; z-index: 3; left: 50%; top: 18px; transform: translateX(-50%);
    margin: 0; max-width: calc(100% - 140px);
    font-family: var(--sans); font-size: .58rem; font-weight: 500;
    letter-spacing: .2em; text-transform: uppercase; text-align: center;
    color: var(--on-noir-2);
    background: rgba(20,17,14,.55); border: 1px solid rgba(242,239,233,.2);
    padding: 8px 14px;
    -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
    pointer-events: none;
    transition: opacity .5s var(--ease-lux);
}
.viewer-hint.is-gone { opacity: 0; }

/* Phones: the arrows would sit on the work at the width where the work is already
   tight, and the swipe does the same job. The bar keeps the counter, so position
   is never lost. */
@media (max-width: 619px) {
    .viewer-arrow { display: none; }
    .viewer-title { font-size: 1rem; }
    .viewer-zoom-btn { min-width: 40px; height: 40px; }
    /* Above the bar rather than under the close button: at this width the hint
       wraps to two lines and the top of the screen is already spoken for. */
    .viewer-hint {
        top: auto; bottom: calc(84px + env(safe-area-inset-bottom, 0px));
        max-width: calc(100% - 32px); letter-spacing: .16em;
    }
}

@media (prefers-reduced-motion: reduce) {
    .viewer-track.is-gliding,
    .viewer-slide img.is-gliding { transition: none; }
}

/* ======================== THE FILM, FULL SCREEN ========================
   The poster used to swap itself for the player in place, which showed a film in
   a column-width box beside the copy — the smallest it could possibly be. It
   opens over the page instead.

   The frame is a 16:9 box capped at 1100px and at the viewport, so the player is
   never taller than the screen on a laptop nor wider than it on a phone; black
   behind it, because that is what letterboxing should look like rather than the
   page's paper showing through the bars. Same dark ground and the same close
   button as the artwork viewer — a visitor should not have to learn two ways out
   of two overlays on one page. */
.film-modal {
    position: fixed; inset: 0; z-index: 300; display: none;
    align-items: center; justify-content: center;
    padding: clamp(14px, 4vw, 52px);
    background: rgba(20, 17, 14, .94);
    -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px);
}
.film-modal.is-open { display: flex; }
.film-modal-frame {
    position: relative; width: 100%;
    max-width: min(1100px, 100%);
    max-height: 100%;
    aspect-ratio: 16 / 9;
    background: #000;
    border: 1px solid var(--frame);
}
.film-modal-frame iframe {
    position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block;
}
/* A short window would otherwise crop the player against the top and bottom
   padding; height leads there and the width follows the ratio. */
@media (max-height: 620px) {
    .film-modal-frame { width: auto; height: 100%; max-width: 100%; }
}

/* ============================== THE SECTION LEAD ==============================
   Three sections — Sourcing, Authentication, Legacy — were a heading and a
   paragraph in a 62-72ch column with the other half of a 1440px screen left
   empty beside them. That is not generous whitespace, it is an unfinished row:
   luxury whitespace is FRAMED, it has something holding its far edge.

   So above 1000px the heading takes a narrow left column and the prose sits
   beside it, and whatever the section carries underneath — the six-step grid,
   the timeline, the button — spans the full width below. It is one device used
   three times, which is what makes it read as a house style rather than as a
   fix; and it is CSS only, so no markup moved and every reveal target,
   anchor and test selector still resolves.

   Below 1000px it collapses back to one column, unchanged. */
@media (min-width: 1000px) {
    .sourcing-inner,
    .authentication > .wrap,
    .legacy > .wrap {
        display: grid;
        grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
        column-gap: clamp(40px, 6vw, 110px);
        align-items: start;
    }
    /* The heading holds the left column and nothing else does, so the brass rule
       under it lands on the page's left axis exactly as it does everywhere else.
       It is the ONLY thing here with an explicit row. */
    .sourcing-inner > h2,
    .authentication > .wrap > h2,
    .legacy > .wrap > h2 { grid-column: 1; grid-row: 1; margin-bottom: 0; }

    /* Everything else is pinned to a COLUMN and left to auto-place its row.
       This is the whole trick, and getting it wrong is how the first version
       broke: pinning `grid-row: 1` as well assumes each section has exactly one
       paragraph. Authentication has three — two before the six-step grid and one
       after it — so all three were stacked into the same cell, printed on top of
       one another. Auto-placement puts each in the next free row of column 2 and
       cannot care how many there are. */
    .sourcing-inner > p,
    .authentication > .wrap > p,
    .legacy > .wrap > .legacy-intro {
        grid-column: 2; margin-top: 0; max-width: 60ch;
    }
    /* Sourcing's button: column pinned, row auto — under its own paragraph. */
    .sourcing-inner > .btn { grid-column: 2; justify-self: start; margin-top: 1.9rem; }

    /* Everything the section carries below the lead spans both columns. */
    .authentication > .wrap > .auth-grid,
    .legacy > .wrap > .timeline { grid-column: 1 / -1; }
    /* And anything AFTER that spans it too — Authentication closes with a line
       under the six checks, which belongs beneath them, not up in the lead. */
    .authentication > .wrap > .auth-grid ~ p { grid-column: 1 / -1; max-width: 72ch; }
}

/* ============================== FILM ==============================
   A click-to-play facade, not an embedded player. Until the visitor clicks there
   is only a poster and a button — no third-party script, no third-party storage.
   ---------------------------------------------------------------------------- */
/* --- THE ONE DARK BAND ------------------------------------------------------
   Everything below inherits by TOKEN OVERRIDE rather than by re-declaring
   colours: redefining --ink, --ink-2 and the hairlines on the section flips every
   descendant rule that was already written against them, and it means a new
   element dropped into the film is correct without anyone remembering to darken
   it. Only the values that genuinely cannot survive a dark ground are named.

   --paper is deliberately NOT overridden: it is the play triangle and the fill
   that light buttons take on hover, and it has to stay light. */
.film {
    /* WAS var(--noir), and the whole block below it was the machinery for that:
       eight token overrides so every descendant rule written against --ink,
       --line or --gilt-text flipped to a dark-ground mix without anyone having to
       remember. They are gone with the dark ground. Leaving even one behind is
       worse than never having had them — an --ink of --on-noir on this band is
       #f2efe9 text on #f4ede7, which is not "hard to read", it is invisible.

       The section keeps NO colour overrides at all now. It is an ordinary light
       band, and every element in it resolves against the page palette like every
       other section, which is why nothing below needed re-colouring by hand. */
    background: var(--film-band);
    /* A dark band wanted more air than a light one. It is not a dark band any
       more, but the film is still the tallest thing on the page and the extra
       15% is what stops the poster crowding the copy beside it. Kept on its own
       merits rather than inherited from a rule that no longer applies. */
    padding-block: calc(var(--section-y) * 1.15);
    /* Guard for the bleed below. Nothing here is sticky, so this is free. */
    overflow: hidden;
}
/* Both of these named the dark mix explicitly because the token override could
   not reach them — .film h2::after and .film-meta span are set from
   --gilt-bright directly. With the band light, the base rules are already right
   and the overrides are deleted rather than re-pointed: --gilt-on-noir is mixed
   to survive #1c1813 and falls to 1.9:1 on #f4ede7.

   --gilt-on-noir and --accent-on-noir are now unused by the page. They stay
   defined in :root — the lightbox and the viewer still sit on dark grounds, and
   the next dark surface should reach for the measured mix rather than picking a
   new one by eye. */
.film-grid { display: grid; grid-template-columns: 1fr; gap: clamp(30px, 4vw, 64px); align-items: center; }
.film-copy h2 { margin-bottom: .6em; }
.film-copy p { color: var(--ink-2); max-width: 44ch; }
.film-copy .link-arrow { margin-top: 1.6rem; }

.film-media { margin: 0; position: relative; }

/* WAS .video-facade, a button that opened a Vimeo overlay. It is now a plain
   frame holding a poster with a silent loop laid over it, so the name, the
   cursor and the hover lift all had to go: nothing here is clickable, and a
   pointer cursor over dead pixels is a promise the page cannot keep.
   Renamed rather than re-purposed on purpose — four other rules and two
   JavaScript selectors reference this frame, and a stale name is how the next
   person spends twenty minutes looking for a button that is not there. */
.film-still {
    display: block; width: 100%; padding: 0; border: 1px solid var(--frame);
    background: var(--noir); position: relative;
    aspect-ratio: 16 / 9; overflow: hidden;
}
.film-still img { width: 100%; height: 100%; object-fit: cover; }

/* The video sits ON the poster, not instead of it, and starts invisible.
   Two reasons, and the second is the one that matters. A <video> paints its
   first frame at some unpredictable moment after the metadata lands, so
   swapping the poster out would flash — poster, nothing, video. And if
   autoplay is refused, the element is removed and the poster underneath was
   never disturbed. Only `playing` adds .is-playing, so the fade is driven by
   frames actually moving, not by an optimistic play() promise.
   object-fit: cover, because the clip is 1600x720 (20:9) inside a 16:9 frame:
   it is centre-cropped to exactly 1280x720, which is the crop the poster was
   cut to. Letterboxing would put black bars inside a bordered plate. */
.film-still video {
    position: absolute; inset: 0; width: 100%; height: 100%;
    object-fit: cover; opacity: 0; transition: opacity 1.2s var(--ease-lux);
}
.film-still video.is-playing { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
    .film-still video { transition: none; }
}

/* REMOVED: .video-play (the brass disc) and .video-duration (the "1:37" badge).
   Both labelled a click-to-play Vimeo film. There is no film, no click and no
   duration — a loop has no running time — and a play button drawn over
   something that will not play is worse than no button at all. The markup that
   used them is gone too; if a real film returns, both rules are in the history
   of this file. */

.film-media.is-playing { aspect-ratio: 16 / 9; }

/* The caption under the poster. A hairline separates it from the still, the way a
   gallery's wall label sits under the frame rather than on it — and the label is
   ordered the way a programme is: what it is, what it is called, how long and who,
   then where to go next. The kicker is the one place on this page besides errors
   and required marks where the accent red appears, and it is doing the same job it
   does there: naming a category, not decorating one. */
.film-caption {
    margin-top: clamp(18px, 2.4vw, 26px);
    padding-top: clamp(18px, 2.4vw, 26px);
    /* --line-2, matching the press strip opposite it. The film sits on a tonal
       band — #f4ede7 now rather than noir — and on a tonal band the paper
       hairline disappears; two rules on one line have to be the same rule. */
    border-top: 1px solid var(--line-2);
}
/* .film-caption-bare KEEPS THE HAIRLINE. It was written to drop it — a rule
   above a lone button looked like a divider with nothing to divide — and
   verify-responsive caught that at all seven desktop widths within a minute.
   The press strip opposite shares this row so that the two rules read as ONE
   line across the section; take one away and the left column has a rule and the
   right column has nothing, which is a worse mistake than the one being fixed.
   The class stays as the hook for anything the caption needs when it is holding
   only the button. */

.film-kicker {
    margin: 0 0 .9rem;
    font-family: var(--sans); font-size: .6rem; font-weight: 500;
    letter-spacing: .22em; text-transform: uppercase;
    color: var(--accent);
}
.film-name {
    margin: 0 0 .55rem;
    font-family: var(--serif); font-size: clamp(1.35rem, 2.4vw, 1.85rem);
    line-height: 1.2; color: var(--ink);
}
.film-meta {
    margin: 0;
    font-family: var(--sans); font-size: .64rem; font-weight: 500;
    letter-spacing: .17em; text-transform: uppercase; color: var(--ink-2);
}
/* --gilt-text, not --gilt-bright. On noir this span was overridden to the dark
   mix and read fine; on #f4ede7 --gilt-bright measures 3.08:1, and the palette
   says so itself — "decorative accents only, too light to sit behind text". A
   middot between two credits is small, but it is text, and --gilt-text is the
   brass mixed to BE text (5.7:1). Caught by verify-composition.js. */
.film-meta span { margin: 0 .5em; color: var(--gilt-text); }
/* Outline, not brass: the page's brass is spoken for by Enquire, and this leads
   OFF the page. It should read as a door, not as the door. */
.film-cta { margin-top: clamp(22px, 2.6vw, 30px); }
@media (max-width: 619px) {
    .film-cta { width: 100%; }
}
.film-media iframe { width: 100%; aspect-ratio: 16 / 9; border: 0; display: block; }

/* ============================== SCROLL REVEAL ============================== */
/* Applied by main.js §0c. The .reveal class is only added when JS runs AND the
   visitor hasn't asked for reduced motion, so content is never hidden by CSS
   alone — if scripting fails, everything is simply visible. */
.reveal { opacity: 0; transform: translateY(22px); }
.reveal.is-in {
    opacity: 1; transform: none;
    transition: opacity .95s var(--ease), transform .95s var(--ease);
}

/* --- three ways in, not one -------------------------------------------------
   A single fade applied to everything is what a template does. Each of these is
   the entrance that kind of thing deserves, and between them they are most of
   the difference between "animated" and "considered". All are applied by
   main.js §0c, never in the markup, so nothing here can hide content if the
   script fails. */

/* A HEADING is wiped up from behind its own baseline. The type arrives rather
   than fading up — the one gesture that reads as typographic rather than as web.
   The clip is slightly beyond 100% at both ends so descenders and the brass of
   an italic are never shaved by a rounding error. */
.reveal-head { clip-path: inset(-10% 0 105% 0); transform: translateY(18px); }
.reveal-head.is-in {
    clip-path: inset(-10% 0 -10% 0); transform: none;
    transition: clip-path 1.1s var(--ease-lux), transform 1.1s var(--ease-lux);
}

/* A RULE draws itself in from the left: the same gesture the nav and footer
   links use on hover, so the page has ONE idea about how a line appears. Used by
   the hero settle (main.js §0c2), which is the page's only <hr class="rule">. */
.reveal-rule { transform: scaleX(0); transform-origin: left center; }
.reveal-rule.is-in {
    transform: none;
    transition: transform .9s var(--ease-lux);
}

/* A WORK is uncovered from the bottom edge, like a cloth coming off it.
   Deliberately NO translate: .reveal's translateY shifts getBoundingClientRect,
   which is exactly what lands an anchor jump 22px off target. A clip moves
   nothing, so a plate can animate anywhere on the page without consequences. */
.reveal-plate { clip-path: inset(0 0 100% 0); }
.reveal-plate.is-in {
    clip-path: inset(0 0 0 0);
    transition: clip-path 1.15s var(--ease-lux);
}

/* A finished clip is REMOVED, not left at zero. `inset(0 0 0 0)` still clips, and
   what it clips is the focus ring: .film-cta is a real link, and a heading
   can contain a link. You cannot transition to `none`, so main.js swaps it in
   once the animation is over — at which point the two look identical. */
.reveal-head.is-done, .reveal-plate.is-done { clip-path: none; }

/* ============================== RESPONSIVE ============================== */
/* --- Narrow phones: letterspacing is what breaks first ------------------- */
@media (max-width: 619px) {
    /* One work per row now, so the medium can stay on the card — there is room for
       it, and it is what a collector reads after the title. */
    .work-caption { margin-top: 12px; padding-top: 10px; }
    .work-title { font-size: 1rem; }
    .work-enquire { margin-top: .7em; }
}

@media (max-width: 619px) {
    /* The private-list band reads as a statement on a phone: the copy centred, and
       the link gone — the sticky bar carries Enquire two thumb-widths below it, and
       two invitations that close together compete rather than reinforce. */
    .footer-private { text-align: center; }
    .footer-private-copy { margin-inline: auto; }
    .footer-private .link-arrow { display: none; }

    /* One work per row on every phone. Two-up was unreadable at 360px and merely
       cramped at 500px; a single column gives each work the whole screen width,
       which is the point of a wall of art.
       The plate's height is capped so nine full-width squares do not make a
       5,000px page — object-fit:contain means nothing is cropped, the mount is
       simply landscape here. */
    /* aspect-ratio AND max-height fight each other: the ratio computes a height
       from the full-width box (346px), max-height clamps the box to 340, and the
       6px difference is clipped off the BOTTOM of the artwork by overflow:hidden.
       A definite height with the ratio switched off leaves nothing to disagree
       about, and the grid row inside stays definite for WebKit. */
    .work-media { aspect-ratio: auto; height: 340px; }
}

@media (max-width: 479px) {
    /* .26em tracking pushed "EST. 1967 · KNIGHTSBRIDGE, LONDON" onto two lines
       with LONDON orphaned, and the same for the longest trust-strip label. */
    .eyebrow { letter-spacing: .15em; }
    .trust-inner span { letter-spacing: .12em; }
    /* Full-width CTAs read as deliberate at this size, rather than a stray box. */
    .hero-actions .btn,
    .sourcing .btn { width: 100%; }
}

@media (max-width: 999px) {
    .site-nav > .nav-cta { margin-top: 16px; }
    .logo img { height: 38px; }
}

@media (max-width: 899px) {
    /* The sticky bar already carries Call, in gallery hours. Two phone numbers a
       thumb apart is clutter, and this one sits directly above the primary CTA. */
    .hero-call { display: none; }
}

/* THE HERO PICTURE ON A PHONE (see the <figure class="hero-mobile-art"> in
   index.php). Hidden by default; shown only where .hero-media is not.
   Centred by align-self — .hero-copy is a flex column, so the figure is a flex
   item and margin:auto would not centre it on its own. Width is capped in px as
   well as vw so a large phone in landscape does not get a poster: 78vw of a
   390px screen is 304px, and the cap only bites above ~460px. A hairline frame,
   no mat — this is a photograph of a room, not a print in a window. */
.hero-mobile-art { display: none; }
@media (max-width: 899px) {
    .hero-mobile-art {
        display: block; margin: clamp(24px, 6vw, 34px) 0 0;
        width: min(78vw, 360px); align-self: center;
    }
    .hero-mobile-art picture, .hero-mobile-art img { display: block; }
    .hero-mobile-art img {
        width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover;
        border: 1px solid var(--frame);
    }
}



@media (min-width: 480px) {
    .expertise-stats { grid-template-columns: 1fr 1fr; }
}

@media (min-width: 620px) {
    .works-grid { grid-template-columns: repeat(3, 1fr); }
    .footer-grid { grid-template-columns: 1fr 1fr; }

    /* The small print spreads out: legal left, the claims centre, the way back
       right — three items on one line rather than a stack. */
    .footer-base { flex-direction: row; justify-content: space-between; align-items: baseline; text-align: left; }
    .footer-private { grid-template-columns: minmax(0, 1fr) auto; gap: 40px; }
    .field-row { grid-template-columns: 1fr 1fr; }
    .trust-inner { grid-template-columns: repeat(3, 1fr); }
    .trust-inner span { border-bottom: 0; border-right: 1px solid var(--line); }
    .trust-inner span:last-child { border-right: 0; }
    .auth-grid { grid-template-columns: 1fr 1fr; }
    .footer-inner { flex-direction: row; justify-content: space-between; }
    .footer-nav { flex-direction: row; gap: 28px; }
}

/* Hero splits earlier than the rest of the layout: stacked, it is simply too tall
   on tablets. Everything else still switches at 1000px. */
@media (min-width: 900px) {
    /* Three columns of prose need real width; below this they are one column. */
    .why-grid { grid-template-columns: repeat(3, 1fr); }

    /* Copy column carries the vertical padding so the noir panel can run the full
       height of the section — flush under the header, flush to the trust strip. */
    .hero { padding-block: 0; }

    /* The first screen is the hero and the trust strip, exactly: no strip of the
       next section showing beneath them, and no dead band above the fold. It was
       leaving a 37-164px gap depending on the laptop.

       The pair is measured together rather than the hero alone, because the trust
       strip is NOT a fixed height — its longest label wraps to two lines between
       roughly 620 and 940px, so subtracting a constant would leave a 20px gap at
       exactly the widths a small laptop uses. Here the wrapper owns the height and
       .hero takes whatever is left (flex: 1), so the strip can be any height it
       likes and the fold still lands on it.

       The vh line is a fallback for browsers without svh — on a desktop the two
       are the same number, and svh is the safer one on anything with a dynamic
       toolbar. If svh is unsupported the second declaration is dropped and the
       first one stands. */
    .first-screen {
        display: flex; flex-direction: column;
        min-height: calc(100vh  - var(--header-h));
        min-height: calc(100svh - var(--header-h));
    }
    /* Grid, because the hero's only child is the .wrap: an auto row in a stretched
       grid grows to fill the container, which carries the extra height down to
       .hero-copy and .hero-media — both of which already centre their contents —
       instead of leaving it as padding at the bottom. */
    .hero { flex: 1 1 auto; display: grid; }
    .hero-copy { padding-block: clamp(48px, 7vh, 104px); }
    .hero-grid { grid-template-columns: 1.06fr .94fr; gap: clamp(40px, 4.5vw, 76px); }

    /* The panel's BOX (not just its background) extends to the viewport edge, so
       the print centres in the dark area a viewer actually sees. Backgrounding the
       bleed alone left the work sitting visibly left of centre. */
    .hero-media {
        margin-right: calc(-1 * var(--bleed));
        padding-inline: var(--hero-pad);
    }
    .hero-media::before { left: 0; right: 0; }

}

/* Short windows: pull the display type down too, or two lines of 90px headline
   plus the plate still overflow a 620px viewport. */
/* Short laptop windows (~600-760px of viewport): pull the display type and rhythm
   in so the whole hero — headline, CTA, artwork and caption — clears the fold. */
@media (min-width: 900px) and (max-height: 820px) {
    /* Every number here is tuned to one hard requirement: on a 13" laptop
       (~600px of viewport) the header, the whole hero AND the three trust
       signals must all sit above the fold. tools/verify-responsive.js asserts
       it — change these and re-run that pass, don't eyeball it.
       Display up, lede down: the headline was competing with its own body copy,
       and it is deliberately NOT the thing that gives way when space is short. */
    .hero-copy { padding-block: clamp(20px, 3.2vh, 80px); }
    .hero h1 { font-size: clamp(3.2rem, 7.8vh, 5rem); }
    .hero .rule { margin: .9rem 0 1.1rem; }
    .hero-copy .lede { font-size: .94rem; line-height: 1.55; max-width: 42ch; }
    .hero-actions { margin-top: 1.3rem; }
    .hero-media { padding-block: clamp(20px, 3.2vh, 60px) clamp(16px, 2.4vh, 44px); }
    .hero-plate img { max-height: min(45vh, 440px); }
}

@media (min-width: 1000px) {
    .spotlight-grid,
    .expertise-top { grid-template-columns: 1fr 1fr; }
    /* Brand column wider than the three link columns. */
    .footer-grid { grid-template-columns: 1.5fr 1fr 1fr 1.2fr; gap: clamp(30px, 3vw, 56px); }
    /* Dormant, deliberately kept. This made the form travel with the reader while
       a long copy column scrolled past it — but the contact block and the hours
       have moved to the footer, so the copy column is now the SHORTER of the two
       and the grid row is exactly as tall as the form. There is nothing to stick
       to, and nothing to fix: a form that outlasts the copy beside it can never
       scroll out of reach. Left here for the day that column grows back. */
    .enquire-form-wrap { position: sticky; top: var(--anchor-offset); }
    .expertise-media { aspect-ratio: auto; height: 100%; min-height: 340px; }
    /* Film leads with the work on screen, so it takes the wider column. */
    /* Two columns, TWO ROWS, with both columns dissolved into the grid.
       The press strip and the film's wall label are the second row of their
       respective columns, and each carries a hairline above it. Left as two
       independent columns those hairlines land wherever their own content happens
       to end — nine pixels apart, which is close enough to read as a mistake
       rather than as two separate rules. Sharing a grid row puts them on one line
       by construction, at any width, whatever the copy is changed to.

       `display: contents` is what makes that possible without moving the
       figcaption out of its figure: the wrappers stop generating boxes and their
       children become grid items directly. Hence .film-intro in index.php — this
       column needs exactly two blocks to place, not four. */
    .film-grid {
        grid-template-columns: .85fr 1.15fr;
        grid-template-rows: auto auto;
        align-items: start;
        row-gap: 0;
    }
    .film-copy, .film-media { display: contents; }
    .film-intro   { grid-column: 1; grid-row: 1; align-self: center; }
    .press        { grid-column: 1; grid-row: 2; }
    /* The poster runs OFF the right edge of the screen. Every other section on
       this page starts and stops at the same two vertical lines, and a page with
       one measure and one rhythm reads as a template however good the parts are.
       This is the single place the pace changes — and it costs no new asset.
       --bleed is exactly the distance from the wrap's content edge to the
       viewport edge (the same maths the header nav uses), so the poster reaches
       the edge and stops: it cannot produce a horizontal scrollbar, and
       verify-responsive.js checks that at eighteen widths.
       The CAPTION deliberately does not bleed. A full-bleed image with its wall
       label held in the text column is how a catalogue sets one; bleeding both
       would just make the label hard to read. */
    /* width:auto matters. The facade carries `width: 100%` for the mobile
       layout, and a percentage width resolves against the grid AREA — so the
       negative margin moved nothing and the poster still stopped at the wrap.
       Released to `auto` it stretches to the track minus its margins, which is
       the track plus the bleed: exactly the viewport edge. */
    .film-still { grid-column: 2; grid-row: 1; width: auto; margin-right: calc(-1 * var(--bleed)); }
    .film-caption { grid-column: 2; grid-row: 2; }
    .enquire-grid { grid-template-columns: 1fr 1fr; }
    .expertise-stats { grid-template-columns: repeat(4, 1fr); }
    .auth-grid { grid-template-columns: repeat(3, 1fr); }
    .lightbox-inner { grid-template-columns: 1.2fr 1fr; grid-template-rows: minmax(0, 1fr); }
    /* Desktop: arrows sit at the screen edges, clear of the work entirely. */
    .lb-nav { bottom: auto; top: 50%; transform: translateY(-50%); }
    .lb-prev { left: clamp(10px, 2.5vw, 40px); }
    .lb-next { left: auto; right: clamp(10px, 2.5vw, 40px); }
    .lb-zoom img { max-height: 60vh; }
    .timeline-item { grid-template-columns: 8rem 1fr; }

}


/* ============================ THE DESKTOP NAV ROW ============================
   1100px, NOT the 1000px the rest of the desktop layout uses, and the extra
   hundred pixels are the whole reason this block was lifted out.

   The row went from five items to seven (Townhouse and Exhibitions were sections
   with no way into them from the header). At 1000px the seven no longer fit
   between the wordmark and the page edge: the page grew a horizontal scrollbar,
   and — the subtler failure — the nav was pushed past the line it is supposed to
   land on. Enquire's right edge and the hairline above the caption under the hero
   work are ONE vertical line, and at 1000px it was 36px out. Shrinking the type
   far enough to fix that put the labels at 8px, which is not a size to set a menu
   in for an audience of collectors in their sixties.

   So between 1000 and 1099 the menu button stays and the panel does the work,
   which is what it is for. Everything else about the desktop layout still starts
   at 1000; only the nav waits.
   ========================================================================== */
@media (min-width: 1100px) {
    /* THE STICKY BAR RETIRES HERE, not at 1000, and it must be this breakpoint and
       no other. Its whole job is to keep an Enquire button on screen where the
       header has none. Moving the nav row to 1100 and leaving this at 1000 left
       1000-1099 with a hamburger, no header CTA and no bar — a hundred pixels of
       laptop width with no persistent way to enquire on a page whose only goal is
       enquiries. Caught by verify-responsive.js, which knew the bar had to be
       present wherever the row was not. */
    .mobile-cta { display: none; }   /* from here the header carries the CTA */

    .site-nav {
        position: static; display: flex; flex-direction: row; align-items: center;
        /* Was clamp(18px, 2vw, 30px), sized for five links. Seven need to be able
           to close up further on a laptop before the row gives out; the vw term
           still opens them back out on a wide screen. */
        gap: clamp(10px, 1.9vw, 30px); background: none; border: 0; padding: 0;
        /* The nav reaches PAST the wrap, so Enquire's right edge lands on the hero
           panel's content edge — the same vertical line as the hairline above the
           caption under the work. Inside the wrap the button stopped a long way
           short of it: at 1920px, 328px short, which read as the header being
           narrower than the page. The max() is what keeps it honest below ~1240px,
           where the wrap gutter is already tighter than the panel's padding and
           shifting further out would push the nav past the artwork instead of onto
           its edge. The logo stays on the wrap, where the copy column starts. */
        margin-right: calc(-1 * max(0px, var(--bleed) - var(--hero-pad)));
    }
    /* Resizing past the breakpoint with the panel open closes it; the closing state
       must not follow the nav into its desktop row and blank it for a quarter second. */
    .site-nav.is-closing {
        height: auto; overflow: visible; padding: 0;
        pointer-events: auto; animation: none;
    }
    /* NEVER WRAP A NAV LABEL. Two words on two lines — "THE / TOWNHOUSE" — reads
       as two menu items, and with seven of them the row turns into a paragraph.
       Adding the Townhouse and Exhibitions links pushed it over at ~1280px, which
       is a laptop, not an edge case. The gap absorbs the difference instead: it
       is what should give when the row is tight, because the row's job is to be
       one line. Below 1000px the whole nav becomes the full-screen menu anyway,
       and there it is a column and wrapping is fine. */
    /* .68rem was sized for five links; seven overflowed the page below ~1300px
       and produced a horizontal scrollbar, which verify-responsive.js checks for
       at eighteen widths. The type steps down with the viewport and is back at
       .68rem by 1500px, where there is room for it again. */
    .site-nav > a {
        padding: 0; border: 0; white-space: nowrap;
        font-size: clamp(.56rem, .30rem + .42vw, .68rem);
        /* Tracking gives before size does — uppercase at .12em is still clearly
           tracked, whereas another half-point off the size starts to look like a
           different typeface from the rest of the page. */
        letter-spacing: clamp(.12em, .04em + .12vw, .18em);
    }

    /* Draws in from the left on enter, retracts to the right on leave. The
       transform-origin swap is what makes it feel drawn rather than toggled. */
    .site-nav > a:not(.nav-cta) { position: relative; }
    .site-nav > a:not(.nav-cta)::after {
        content: ""; position: absolute; left: 0; right: 0; bottom: -7px; height: 1px;
        background: var(--gilt-bright);
        transform: scaleX(0); transform-origin: right center;
        transition: transform .55s var(--ease-lux);
    }
    .site-nav > a:not(.nav-cta):hover::after,
    .site-nav > a:not(.nav-cta):focus-visible::after {
        transform: scaleX(1); transform-origin: left center;
    }
    .nav-toggle { display: none; }   /* horizontal nav replaces it — without this
                                        the hamburger sits next to the full nav */
}

@media (min-width: 1200px) {
    /* Four to a row on a full desktop: nine works land as two clean rows and a
       remainder, which reads as a catalogue page rather than a scroll. */
    .works-grid { grid-template-columns: repeat(4, 1fr); }
}

/* ====================== Smooth scrolling (Lenis 1.3.26) ======================
   The whole-page glide is provided by Lenis, self-hosted at
   assets/js/vendor/lenis-1.3.26.min.js and initialised in assets/js/main.js.

   Header offset: html{scroll-padding-top:var(--anchor-offset)} above is the single
   source of truth. Lenis's scrollTo() reads scroll-padding-top off the root and
   scroll-margin-top off the target, so anchors clear the sticky header with no
   hardcoded numbers in JS. Do NOT also put scroll-margin-top on #enquiry-form —
   the two values stack and leave a large gap above the form.

   To tune the feel, change `lerp` in main.js. To turn the effect off entirely,
   remove the vendor <script> tag from index.php; everything degrades to the
   native scroll-behavior:smooth fallback above. ---------------------------- */

/* Lenis animates scroll itself; leaving native smooth on fights it. */
html.lenis { scroll-behavior: auto; }

/* --- vendored from lenis@1.3.26 dist/lenis.css — re-copy verbatim on upgrade --- */
html.lenis,
html.lenis body { height: auto; }

.lenis:not(.lenis-autoToggle).lenis-stopped { overflow: clip; }

.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch],
.lenis [data-lenis-prevent-vertical],
.lenis [data-lenis-prevent-horizontal] { overscroll-behavior: contain; }

.lenis.lenis-smooth iframe { pointer-events: none; }

.lenis.lenis-autoToggle {
    transition-property: overflow;
    transition-duration: 1ms;
    transition-behavior: allow-discrete;
}
/* --- end vendored lenis.css --- */

/* ============================== REDUCED MOTION ============================== */
@media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    *, *::before, *::after {
        animation-duration: .001ms !important;
        transition-duration: .001ms !important;
    }
    .reveal, .reveal-head, .reveal-rule, .reveal-plate {
        opacity: 1; transform: none; clip-path: none; transition: none;
    }
}

/* THE PRICE ON THE CARD (3 Oct 2026). Set in the ink colour at the edition
   line's size, not in brass and not larger — a price shouted reads as a sale
   tag; stated plainly beside the edition it reads as a catalogue entry, which is
   the register the rest of the card is in. Tabular figures so £12,000 and
   £220,000 align across a row of cards. */
.work-price {
    margin: .35em 0 0;
    font-family: var(--sans); font-size: .82rem; font-weight: 500;
    letter-spacing: .02em; color: var(--ink);
    font-variant-numeric: tabular-nums lining-nums;
}
