/* =====================================================================
   Andipa Gallery — Original Banksy landing page
   Mobile-first. Breakpoints: base (phone) -> 720px (tablet) -> 1000px (desktop)
   ===================================================================== */

/* ------------------------------ Tokens ----------------------------- */
:root{
  --bg:      #f4f4f4;   /* warm off-white page */
  --panel:   #ede7dc;   /* cream bands (advisory / footer) */
  --ink:     #1c1a17;   /* near-black warm text / CTAs */
  --muted:   #57534b;   /* secondary text (AA on --bg and --panel) */
  --line:    #d8d1c4;   /* hairline dividers */
  --accent:  #b23127;   /* restrained Banksy red — used sparingly */
  /* Antique gold. Sampled from the client's reference (#7e673d) and used for
     the FILL of primary actions — the solid sticky button, and the sweep that
     fills every outlined button on hover. It carries 5.4:1 against white, so
     white type on it clears AA at any size. --gold-deep is its pressed/hover
     state; it is the gold darkened, not a second colour. */
  --gold:      #7e673d;
  --gold-deep: #6a5632;
  --white:   #fffefb;

  --display: "Cormorant Garamond", Georgia, "Times New Roman", serif;
  --body:    "EB Garamond", Georgia, "Times New Roman", serif;
  --label:   "Jost", -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;

  --wrap: 1200px;
  --pad-block: clamp(64px, 9vw, 64px);   /* uniform section rhythm — generous, gallery-like */
  --sticky-h: 64px;

  /* ---- Label scale ----------------------------------------------------
     The small-caps labels had drifted to SEVEN sizes between 10.44 and 14.4px
     — near-identical values that read as sloppy rather than as a hierarchy.
     Three steps now, and every label uses one of them:
       xs  micro credits and column heads
       sm  the standard caption (years, counts, links under content)
       md  buttons and footer navigation
     Add a label? Pick a step. Do not introduce a fourth. */
  --fs-label-xs: .62rem;
  --fs-label-sm: .72rem;
  --fs-label-md: .8rem;
}

/* --------------------------- Scrollbar ------------------------------
   The default OS bar is a grey slab against a warm off-white page. This is a
   slim, warm one that matches the hairlines: track disappears into the page,
   thumb is the same --line as every rule on the site.
   Firefox gets the two standard properties; WebKit/Blink get the pseudo
   elements. Nothing here changes scroll BEHAVIOUR, only its appearance, and a
   browser that supports neither simply keeps its own bar. */
html{scrollbar-width:thin;scrollbar-color:var(--line) transparent}
::-webkit-scrollbar{width:10px;height:10px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:var(--line);border-radius:99px;
  /* Transparent border + background-clip keeps the thumb visually slim while
     the hit area stays a comfortable 10px. */
  border:3px solid transparent;background-clip:content-box}
::-webkit-scrollbar-thumb:hover{background:var(--muted);background-clip:content-box}
::-webkit-scrollbar-corner{background:transparent}

/* ------------------------------ Reset ------------------------------ */
*,*::before,*::after{box-sizing:border-box}
html{font-size:18px;-webkit-text-size-adjust:100%}
body{
  margin:0;
  font-family:var(--body);
  font-weight:400;
  font-size:1rem;
  line-height:1.75;
  color:var(--ink);
  background:var(--bg);
  padding-bottom:calc(var(--sticky-h) + env(safe-area-inset-bottom)); /* room for sticky bar */
}
img{max-width:100%;height:auto;display:block}
a{color:inherit;text-decoration:none}
h1,h2,h3{margin:0;font-family:var(--display);font-weight:600;line-height:1.12}
p{margin:0 0 1rem}
ul,ol{margin:0;padding:0;list-style:none}

.wrap{width:100%;max-width:var(--wrap);margin-inline:auto;padding-inline:clamp(20px,5vw,48px)}

.skip-link{position:absolute;left:-999px;top:0;background:var(--ink);color:#fff;padding:.6rem 1rem;z-index:200}
.skip-link:focus{left:8px;top:8px}

:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* ------------------------------ Icons ------------------------------
   One hairline set (see icon() in index.php). Every icon is stroked, never
   filled, and `vector-effect:non-scaling-stroke` pins the line to exactly 1px
   whatever the box is — the same 1px as the section rules, the card hairlines
   and .faq-icon. That single rule is what makes a 10px "opens in a new tab"
   arrow and a 16px lightbox chevron look like they were drawn by the same
   hand.

   Square caps and mitred joins, not round: round caps are the consumer-app
   default and read soft. `geometricPrecision` stops the browser snapping a
   1px diagonal onto the pixel grid, which is what makes a hairline cross look
   broken at one size and fine at the next.

   `currentColor` throughout, so an icon inverts with its button on hover
   without a second rule anywhere. */
.icon{display:block;flex:0 0 auto;fill:none;stroke:currentColor;
  stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;
  shape-rendering:geometricPrecision}
.icon path,.icon line,.icon polyline{vector-effect:non-scaling-stroke}
/* One exception to the hairline set: a play mark. A 9px outlined triangle
   reads as a smudge; this is the only glyph on the page that is filled. */
.icon.icon-solid path{fill:currentColor;stroke:none}
/* Every icon that sits inside text (the new-tab arrow, back to top) rides on
   the baseline rather than the line box, so it sits with the cap height of the
   word beside it instead of dropping below it. */
.nav-external-mark{display:inline-flex;align-items:center;vertical-align:baseline;
  position:relative;top:1px;margin-left:.12em}

/* The one kind of link that runs inside a paragraph (currently the BBC piece
   in the Why section). It has to read as a link without shouting: the rule
   sits a little under the baseline in the hairline colour and only warms to
   gold on hover, so the paragraph still scans as prose. */
.text-link{color:inherit;text-decoration:underline;text-decoration-thickness:1px;
  text-underline-offset:.22em;text-decoration-color:var(--line);
  transition:color .3s ease,text-decoration-color .3s ease}
.text-link:hover,.text-link:focus-visible{color:var(--gold);text-decoration-color:var(--gold)}

/* ---------------------------- Buttons ------------------------------ */
.btn{
  position:relative;overflow:hidden;z-index:0;
  display:inline-block;font-family:var(--label);font-weight:400;
  font-size:var(--fs-label-md);letter-spacing:.15em;text-transform:uppercase;
  color:var(--ink);background:transparent;border:1px solid var(--ink);
  padding:.92rem 2.1rem;cursor:pointer;
  transition:color .45s cubic-bezier(.22,.61,.36,1),border-color .45s cubic-bezier(.22,.61,.36,1);
  text-align:center;line-height:1.2;
  /* Kills the 300ms tap delay and double-tap-zoom on iOS, so a single tap on
     e.g. the sticky Call button registers as a click straight away. */
  touch-action:manipulation;-webkit-tap-highlight-color:transparent;
}
/* Hover: ink sweeps up from the bottom behind the label (shared motion + easing
   with the nav underline, so buttons and links feel like one system). */
/* The sweep is GOLD, not ink. At rest every button is still an ink hairline
   with ink type — the page looks unchanged until you touch something — and the
   moment one is activated the fill that arrives is the gallery's gold. That is
   the whole brand shift: it lives in the action, not in the furniture. */
.btn::before{content:"";position:absolute;inset:0;z-index:-1;background:var(--gold);
  transform:translateY(100%);transition:transform .45s cubic-bezier(.22,.61,.36,1)}
/* HOVER MUST BE POINTER-ONLY. On iOS the first tap of an element carrying a
   :hover rule is spent applying that hover state rather than activating the
   element — which is why the sticky "Call" (a tel: link) appeared to do nothing
   on the first tap. Keyboard focus keeps the same treatment on every device. */
/* The border goes gold with the fill, or a 1px ink outline is left drawn
   around a gold face and the edge reads muddy. */
.btn:focus-visible{color:var(--white);border-color:var(--gold)}
.btn:focus-visible::before{transform:translateY(0)}
@media (hover:hover){
  .btn:hover{color:var(--white);border-color:var(--gold)}
  .btn:hover::before{transform:translateY(0)}
}
/* .btn-lg used to set .76rem against the base button's .8rem — the LARGE
   button carried SMALLER type than the standard one. It is large through
   padding and letter-spacing now; the type stays on the scale. */
.btn-lg{padding:1.05rem 2.5rem;letter-spacing:.17em}
.btn-sm{padding:.5rem 1rem;font-size:var(--fs-label-sm)}
.btn-full{width:100%}
.btn-ghost{border-color:var(--line);background:var(--white)}
.btn-ghost:focus-visible{background:var(--gold);color:var(--white);border-color:var(--gold)}
@media (hover:hover){ .btn-ghost:hover{background:var(--gold);color:var(--white);border-color:var(--gold)} }

/* Mobile: slightly smaller buttons (fuller on desktop). Keep the form submit
   + sticky-bar buttons comfortably tappable. */
@media(max-width:719px){
  /* One step down on a phone — .72rem against .8rem — and tighter padding with
     it. At desktop sizes these buttons sat heavier than the headings beside
     them; small caps at 13px with wide tracking still read as a button and
     stop competing with the type they sit under.
     Still one size for all three: three buttons within 0.04rem of each other
     read as an accident, not as a hierarchy. .btn-lg is large through PADDING. */
  .btn{font-size:var(--fs-label-sm);padding:.6rem 1.15rem;letter-spacing:.13em}
  .btn-lg{padding:.68rem 1.5rem}
  .btn-full{padding:.78rem 1rem;letter-spacing:.14em}
  /* 44px tall — the tap-target floor, not a pixel more. */
  .sticky-cta .btn{padding:.7rem 1rem;letter-spacing:.14em}
}

.eyebrow{font-family:var(--label);font-size:.66rem;letter-spacing:.26em;text-transform:uppercase;color:var(--muted);margin:0 0 1.1rem}
/* The gap UNDER a section head is 1.25em — about 48px at full size — and it is
   the same under every one of them. It was .5em (17px measured) against ~96px
   of section padding above, which read as a heading dropped on top of its own
   text. Two sections had grown their own larger gaps to compensate (the FAQ at
   46px, the testimonials at 40px) which is what made the set look unrelated;
   both of those local overrides are gone now, so this single value governs all
   six headings. Change it here and the whole page moves together. */
.section-head{font-family:var(--display);font-weight:400;font-size:clamp(1.95rem,3.9vw,2.9rem);letter-spacing:-.012em;text-align:center;margin:0 0 50px}
/* Was a flat 1.06rem, which made the intro 19px on a phone against 18px body
   copy — the one place where small screens got the BIGGER type. */
.section-intro{max-width:620px;margin:0 auto 3rem;text-align:center;color:var(--muted);
  font-size:clamp(1rem,1.4vw,1.06rem);line-height:1.72;padding-bottom:64px}

/* Laptop-shaped viewports — wide but SHORT. The uniform 124px section padding
   plus a centred four-line intro pushed the first row of works ~394px below the
   top of the section, so on a 594px-tall window a visitor arrived at "The
   Collection" and saw a heading and a paragraph and no artwork at all.
   Width-based clamps cannot see this; the constraint is viewport HEIGHT, so it
   is keyed off `max-height`. Applied page-wide, since every section had the
   same problem to a lesser degree. */
@media (min-width:1000px) and (max-height:900px){
  :root{--pad-block:clamp(52px,8vh,84px)}
  /* Short screens are usually WIDE screens, so spend the spare width to buy
     back height: a wider measure turns the seven-line legacy intro into five. */
  /* .enquire-intro h2 must ride along, or the headings diverge on short
     laptops (52px against 42px) — the exact drift item 3 was about. */
  .section-head{font-size:clamp(1.8rem,3.2vw,2.35rem);margin-bottom:1em}
  .section-intro{max-width:880px;font-size:1rem;line-height:1.6;
    margin-bottom:clamp(0.2rem, 2.6vh, 2rem)}
}

/* ---------------------------- Header ------------------------------- */
.site-header{position:sticky;top:0;z-index:100;background: rgba(244,244,244,.92);backdrop-filter:saturate(150%) blur(6px);border-bottom:1px solid var(--line);
  /* See .is-stowed below. The transition lives here so the header animates in
     BOTH directions from one declaration. */
  transform:translateY(0);
  transition:transform .55s cubic-bezier(.22,.61,.36,1),opacity .4s cubic-bezier(.22,.61,.36,1),
             visibility 0s linear 0s}

/* 1px, pulled straight back out of the flow: see the comment on the element
   itself in index.php. */
.stow-sentinel{height:1px;margin-bottom:-1px}

/* ---- The header stows itself at the collection ----------------------
   From "The Curated Collection" down, the navigation is gone: past that point
   the visitor is looking at the works, and a bar following them down the page
   is the gallery walking a pace behind a collector. It comes back when they
   return above the collection — not on any upward flick, which is the fidgety
   behaviour most "smart" headers have and the reason they read as cheap.

   `visibility` is what makes it genuinely gone rather than merely off-screen:
   without it the nav links stay in the tab order and a keyboard user lands on
   an invisible menu. It flips on a delay on the way out so it only takes
   effect once the slide has finished, and immediately on the way back in.
   `translateY(-101%)` rather than -100% so the bottom hairline clears too. */
.site-header.is-stowed{transform:translateY(-101%);opacity:0;visibility:hidden;
  transition:transform .55s cubic-bezier(.22,.61,.36,1),opacity .4s cubic-bezier(.22,.61,.36,1),
             visibility 0s linear .55s}
@media (prefers-reduced-motion: reduce){
  .site-header{transition:none}
  .site-header.is-stowed{transition:none}
}
.header-inner{display:flex;align-items:center;justify-content:space-between;gap:clamp(1.5rem,4vw,3.5rem);min-height:66px}
.logo{display:flex;flex-direction:column;line-height:1}
.logo-mark{font-family:var(--label);font-weight:500;font-size:1.15rem;letter-spacing:.34em}
.logo-sub{font-family:var(--label);font-weight:400;font-size:.58rem;letter-spacing:.42em;color:var(--muted);margin-top:2px}

/* Header logo: centre GALLERY under ANDIPA (padding offsets the trailing letter-spacing) */
.site-header .logo{align-items:center}
.site-header .logo-mark{padding-left:.34em}
.site-header .logo-sub{padding-left:.42em}

/* Logo images (header + footer) — constrain height */
/* flex:0 0 auto — the logo is a flex item of .header-inner and defaults to
   flex-shrink:1, so the nav was squeezing it out of ratio (144x40 rendered as
   59x40 from 1920px up). max-width:none stops any inherited img cap doing the
   same thing. */
.site-header .logo{flex:0 0 auto}
.site-header .logo img{height:clamp(30px,4.4vw,40px);width:auto;max-width:none;display:block}
.footer-logo img{height:clamp(40px,6vw,54px);width:auto;display:block}

.nav-desktop{display:none;align-items:center;gap:clamp(.85rem,1.5vw,1.5rem)}
/* Nav shares the exact "UI caps" type of .btn (.74rem / .15em) so the menu and
   the buttons read as one system — the big contrast is with the display serif,
   not between these two. */
.nav-desktop a{position:relative;font-family:var(--label);font-size:.74rem;letter-spacing:.15em;text-transform:uppercase;color:var(--ink);white-space:nowrap;padding-bottom:3px}
/* Hover: a hairline underline draws in from the left — same easing as the button sweep. */
.nav-desktop a::after{content:"";position:absolute;left:0;right:0;bottom:0;height:1px;background:currentColor;transform:scaleX(0);transform-origin:left;transition:transform .4s cubic-bezier(.22,.61,.36,1)}
.nav-desktop a:hover::after,.nav-desktop a:focus-visible::after{transform:scaleX(1)}
/* Narrow desktop: keep a little tightening so the bar never crowds the logo. */
@media (min-width:1000px) and (max-width:1179px){
  .nav-desktop{gap:.8rem}
  .nav-desktop a{font-size:.7rem;letter-spacing:.11em}
}
/* Utility group — off-site link + phone. Muted and set off by a hairline rule so
   the bar reads as two zones: in-page sections (ink) | utility (grey). */
.nav-desktop .nav-external{margin-left:.55rem;padding-left:1.25rem;border-left:1px solid var(--line);color:var(--muted)}
.nav-desktop .nav-external::after{left:1.25rem}
/* The ↗ marks it as leaving the site, and lifts away on hover. */
.nav-desktop .nav-external > span[aria-hidden]{display:inline-block;font-size:.92em;margin-left:.1em;transition:transform .35s cubic-bezier(.22,.61,.36,1)}
.nav-desktop .nav-external:hover > span[aria-hidden]{transform:translate(2px,-2px)}
.nav-desktop .nav-phone{margin-left:.15rem}
.nav-phone{color:var(--muted)!important}
/* Visually hidden, still read by screen readers. */
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
.nav-cta{text-transform:uppercase}

/* The menu mark is an icon too, so it is the same 1px hairline as every drawn
   icon and every rule on the page — it was 1.5px, which made it the heaviest
   line in the header. Wider gaps (6px) with a thinner line: fewer, finer
   strokes with more air is the whole idea. */
.hamburger{display:inline-flex;flex-direction:column;align-items:flex-end;justify-content:center;gap:7px;width:44px;height:44px;padding:10px;background:none;border:0;cursor:pointer}
.hamburger span{display:block;width:22px;height:1px;background:var(--ink);
  transition:transform .42s cubic-bezier(.22,.61,.36,1),opacity .25s cubic-bezier(.22,.61,.36,1)}
/* 8px = one line (1px) + one gap (7px), so the outer strokes land exactly on
   the middle one before they rotate. */
.hamburger[aria-expanded="true"] span:nth-child(1){transform:translateY(8px) rotate(45deg)}
.hamburger[aria-expanded="true"] span:nth-child(2){opacity:0}
.hamburger[aria-expanded="true"] span:nth-child(3){transform:translateY(-8px) rotate(-45deg)}

/* Open menu fills the screen below the header. As a short panel it left the
   page showing underneath it (and the sticky Call/Enquire bar floating over the
   top), which read as broken. --header-h is published live by main.js. */
.mobile-menu{position:fixed;left:0;right:0;top:var(--header-h,66px);bottom:0;z-index:95;
  display:flex;flex-direction:column;background:var(--bg);
  overflow-y:auto;overscroll-behavior:contain;
  padding-bottom:calc(1.5rem + env(safe-area-inset-bottom))}
.mobile-menu[hidden]{display:none}
.mobile-menu a{font-family:var(--label);font-size:.9rem;letter-spacing:.1em;text-transform:uppercase;padding:1rem clamp(20px,5vw,48px);border-top:1px solid var(--line);flex:0 0 auto}
.mobile-menu a:first-child{border-top:0}

/* Opening animation. CSS ANIMATIONS, not transitions: the panel is toggled with
   the `hidden` attribute, so it goes display:none -> display:flex and a
   transition has nothing to run from. An animation starts the moment the
   element becomes rendered, which is exactly the moment we want it.
   The panel settles first, then the rows arrive one after another — it reads as
   the menu unfolding rather than as a block of links appearing at once. Keep
   the stagger short; past about 60ms a step it starts to feel slow to anyone
   who opens the menu to go somewhere specific. */
@keyframes menuPanelIn{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
@keyframes menuRowIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.mobile-menu{animation:menuPanelIn .34s cubic-bezier(.22,.61,.36,1) both}
.mobile-menu > a,.mobile-menu .menu-foot{
  animation:menuRowIn .46s cubic-bezier(.22,.61,.36,1) both}
.mobile-menu > a:nth-child(1){animation-delay:.06s}
.mobile-menu > a:nth-child(2){animation-delay:.10s}
.mobile-menu > a:nth-child(3){animation-delay:.14s}
.mobile-menu > a:nth-child(4){animation-delay:.18s}
.mobile-menu > a:nth-child(5){animation-delay:.22s}
.mobile-menu > a:nth-child(6){animation-delay:.26s}
.mobile-menu > a:nth-child(7){animation-delay:.30s}
.mobile-menu .menu-foot{animation-delay:.30s}
@media (prefers-reduced-motion: reduce){
  .mobile-menu,.mobile-menu > a,.mobile-menu .menu-foot{animation:none}
}
.menu-foot{margin-top:auto;padding:1.4rem clamp(20px,5vw,48px) .4rem;border-top:1px solid var(--line)}
/* Live open/closed line, left-aligned here rather than centred as in the
   sticky bar. It replaces the four-row hours table that used to sit here:
   on a phone that was small print answering a question nobody asked, where
   what a visitor wants to know is simply whether they can come now. */
/* Scoped to .menu-foot deliberately: it overrides .cta-status, which is defined
   far LATER in this file, and a same-specificity rule placed earlier would lose
   the cascade (the trap that has caught several rules on this page). The extra
   class makes it win on specificity instead of on position. */
.menu-foot .menu-state{justify-content:flex-start;margin:0 0 .7rem;font-size:var(--fs-label-xs)}
.menu-foot-address{margin:0 0 .9rem;font-size:.9rem;line-height:1.55;color:var(--ink)}
/* The number used to be the last item of the section list, stranded under
   "Contact". Grouped with the address and the email it reads as contact
   details instead of as a navigation item. */
.menu-contact{list-style:none;margin:0 0 1rem;padding:0;display:flex;flex-direction:column;gap:.3rem}
.menu-contact a{display:inline;padding:0;border:0;font-family:var(--body);font-size:.9rem;
  letter-spacing:0;text-transform:none;color:var(--ink);border-bottom:1px solid var(--line)}
@media (hover:hover){ .menu-contact a:hover{border-bottom-color:var(--ink)} }
.menu-foot-line{display:flex;justify-content:space-between;gap:1rem;margin:0 0 .45rem;
  font-family:var(--label);font-size:.66rem;letter-spacing:.1em;color:var(--muted);line-height:1.5}
.menu-foot-line:first-child{display:block;margin-bottom:1rem;color:var(--ink)}
.menu-foot-line span{color:var(--ink)}
.menu-foot-note{font-family:var(--label);font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--muted);margin:1rem 0 0}
/* The page must not scroll behind the open menu. */
body.menu-open{overflow:hidden}
/* The sticky bar duplicates what the open menu already offers, and sat on top
   of it. Hide it while the menu is open. */
body.menu-open .sticky-cta{display:none}

/* Desktop: show the full nav, hide the hamburger + mobile dropdown */
@media(min-width:1000px){
  .nav-desktop{display:flex}
  .hamburger{display:none}
  .mobile-menu{display:none}
}

/* ----------------------------- Hero -------------------------------- */
.hero{padding-block:clamp(32px,4.5vw,68px) 0}
.hero-inner{display:flex;flex-direction:column;gap:2rem}
.hero-kicker{font-family:var(--label);font-size:.7rem;font-weight:400;letter-spacing:.24em;text-transform:uppercase;color:var(--muted);margin:0 0 1.3rem}
.hero-copy h1{font-size:clamp(2.4rem,5.4vw,3.9rem);font-weight:300;letter-spacing:-.02em;line-height:1.04}
.hero-lead{max-width:38ch;color:var(--muted);font-size:clamp(1.02rem,1.4vw,1.15rem);line-height:1.6;margin:1.5rem 0 2rem}
.hero-media{position:relative;border:1px solid var(--line);box-shadow:0 26px 64px -34px rgba(28,26,23,.32)}
.hero-media img{width:100%;object-fit:cover;aspect-ratio:16/11;background:var(--panel)}
/* Muted looping video (added by main.js on desktop) fades in over the poster image. */
.hero-media video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:0;transition:opacity 1.2s ease}
.hero-media video.is-playing{opacity:1}

/* MOBILE: the whole hero — kicker, headline, lead, button AND the picture —
   inside one screen.
   Sized in `svh`, not `vh`. `vh` is the viewport with the browser chrome
   HIDDEN, so a hero built to 100vh is always taller than what a phone actually
   shows and the last element sits under the address bar. `svh` is the small
   viewport — chrome visible — which is the state a visitor arrives in. That
   single unit is the whole fix; the trims below are what buy the room for it. */
@media(max-width:719px){
  .hero{padding-block:clamp(6px,2svh,28px) 0}
  /* The LOWER bound of each clamp is what a very short phone gets. They are
     deliberately tight — an iPhone SE has ~553px of small-viewport height and
     the picture has to come out of the same budget. */
  .hero-inner{gap:clamp(.6rem,3svh,2rem)}
  .hero-kicker{font-size:.62rem;letter-spacing:.2em;margin:0 0 clamp(.45rem,1.6svh,1.3rem)}
  .hero-lead{font-size:clamp(.92rem,3.6vw,1.02rem);line-height:1.55;
    margin:clamp(.4rem,1.8svh,1.5rem) 0 clamp(.6rem,2.4svh,2rem)}
  /* .btn-lg's own mobile padding is enough now the type is a step smaller. */

  /* The picture scales down WHOLE on short screens by capping the box's WIDTH
     to what its own 16/11 ratio allows in the height available — then the
     ratio does the rest. Capping the image's HEIGHT instead would crop it:
     the img is object-fit:cover, so a squeezed box eats the top and bottom of
     the room rather than shrinking it. Centred, so a narrower picture on a
     very short phone still sits under the text properly. */
  /* 32svh, down from 34. The headline is back at its full size — it is the
     first thing anyone reads and shrinking it to win space was the wrong trade
     — so the picture gives up that room instead. */
  .hero-media{width:min(100%, calc(clamp(150px,32svh,320px) * 16 / 11));
    margin-inline:auto}
}

/* Genuinely short phones (an iPhone SE has about 553px of small-viewport
   height). The clamps above do not help here — their `svh` middle terms are
   still above their own minimums at this size, so the minimums never engage.
   The picture gives up the last 20px, because on the shortest screen it is the
   element with the most to spare. */
@media(max-width:719px) and (max-height:620px){
  .hero-media{width:min(100%, calc(27svh * 16 / 11))}
  /* The headline holds its size here too; only the picture gives way. */
}

@media(min-width:1000px){
  .hero-inner{flex-direction:row;align-items:center;gap:3rem;padding-top:.5rem}
  .hero-copy{flex:0 0 44%}
  .hero-media{flex:1}
  /* Cap the hero image so the whole hero (incl. CTA) fits a laptop viewport. */
  .hero-media img{aspect-ratio:3/2;max-height:62vh}
}

/* -------------------------- Trust strip ---------------------------- */
.trust{border-top:1px solid var(--line);border-bottom:1px solid var(--line);margin-top:clamp(36px,6vw,72px)}
/* Three credentials. Stacked on phones (three across cannot hold "Pest Control"
   on one line at 390px), one even row from 600px up. */
.trust-grid{display:grid;grid-template-columns:1fr;gap:0}
.trust-grid>div{display:flex;flex-direction:column;align-items:center;text-align:center;gap:0;
  padding:1.9rem 1rem 1.8rem;border-top:1px solid var(--line);border-left:0}
.trust-grid>div:first-child{border-top:0}
/* Value first — it always occupies exactly one line (nowrap), so the value row,
   the hairline and the first label line all sit at the SAME height in every
   cell, however the labels wrap below. (The old label-first order needed a
   min-height hack and still drifted out of line.) */
.trust-v{font-family:var(--display);font-size:clamp(1.15rem,1.9vw,1.45rem);font-weight:500;
  letter-spacing:.005em;line-height:1.2;color:var(--ink);white-space:nowrap;
  transition:letter-spacing .5s cubic-bezier(.22,.61,.36,1)}
.trust-k{font-family:var(--label);font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;
  color:var(--muted);line-height:1.55;max-width:22ch;text-wrap:balance}
/* Catalogue rule: a short hairline between value and label, drawn at a fixed
   height in every cell. It is what makes the credentials read as one engraved
   line rather than stacked blocks — and it draws itself in on reveal. */
.trust-k::before{content:"";display:block;width:100%;max-width:28px;height:1px;
  background:var(--ink);opacity:.28;margin:.6rem auto .7rem;
  transform:scaleX(0);transform-origin:center;
  transition:transform .9s cubic-bezier(.22,.61,.36,1) .25s,opacity .5s ease}
.trust-grid>div.is-visible .trust-k::before,
html:not(.reveal-on) .trust-k::before{transform:scaleX(1)}
@media (prefers-reduced-motion: reduce){
  .trust-k::before{transform:scaleX(1);transition:none}
  .trust-v{transition:none}
}
/* Phones: three centred blocks stacked cost ~485px of scroll for three facts.
   Set as spec rows instead — value left, label right, on one line — which is
   the same pattern as the lightbox .spec-row and ~a third of the height.
   Mobile is the dominant device here, so that scroll matters. */
@media(max-width:599px){
  .trust-grid>div{flex-direction:row;align-items:baseline;justify-content:space-between;
    text-align:left;gap:1rem;padding:1rem 0}
  .trust-v{font-size:1.1rem}
  .trust-k{max-width:none;text-align:right}
  .trust-k::before{display:none}
}
@media(min-width:600px){
  .trust-grid{grid-template-columns:repeat(3,1fr)}
  /* Shallow band. The labels are allowed the full column width rather than a
     narrow ch cap, so "Every work authenticated" sits on ONE line instead of
     two — the credentials spread sideways into space that was already there,
     and the band loses a line of height rather than gaining one. */
  .trust-grid>div{border-top:0;border-left:1px solid var(--line);
    padding:clamp(1.35rem,2.5vw,1.7rem) 1.25rem clamp(1.25rem,2.3vw,1.6rem)}
  .trust-grid>div:first-child{border-left:0}
  .trust-k{max-width:100%}
}
/* Pointer-only: the band answers to the cursor without moving anything. */
@media (hover:hover) and (min-width:720px){
  .trust-grid>div:hover .trust-v{letter-spacing:.045em}
  .trust-grid>div:hover .trust-k::before{opacity:.55}
}

/* ----------------------------- About ------------------------------- */
.about{padding-block:var(--pad-block)}
.about-grid{display:flex;flex-direction:column;gap:2rem}
.about-media img{width:100%;object-fit:cover;aspect-ratio:5/4;background:var(--panel)}
.about-copy h2{font-size:clamp(1.6rem,3.2vw,2.4rem);font-weight:400;letter-spacing:-.012em;margin-bottom:.6em;max-width:18ch}
.about-copy p{color:var(--muted)}
@media(min-width:1000px){
  .about-grid{flex-direction:row;align-items:center;gap:3.5rem}
  .about-media{flex:0 0 46%}
  .about-copy{flex:1}
}

/* -------------------------- Available ------------------------------ */
.available{padding-block:var(--pad-block);background:var(--white);border-block:1px solid var(--line)}
.works{display:grid;grid-template-columns:1fr;gap:clamp(2rem,4vw,3rem) clamp(1.5rem,3vw,2.5rem)}
/* SYMMETRIC padding, so the artwork sits on the true centre of its frame.
   The bottom padding used to be 32px deeper than the top, reserving a clean
   strip for the expand glyph so nothing would overlap the work. It did that,
   but it also pushed every artwork 16px above centre — measured 21px of air
   above and 53px below — and a column of works all hanging high is exactly the
   sort of thing that reads as careless in a gallery.

   The glyph reserve is kept as a HEIGHT CAP on the image instead. Because the
   cap takes the space off both ends of a centred box, the clearance survives
   and the centre does not move. Net effect on the artwork: nothing at all for a
   width-limited work (which is most of them), 8px shorter for a tall one. */
.work-media{--work-pad:clamp(12px,2.4vw,20px);
  /* How far the artwork must stay clear of the BORDER BOX's bottom edge:
     the glyph's reach (6px inset + 28px tall = 34) plus 4px of air. */
  --work-glyph-clear:38px;
  appearance:none;-webkit-appearance:none;margin:0;font:inherit;color:inherit;text-align:inherit;display:block;width:100%;position:relative;aspect-ratio:4/3;
  padding:var(--work-pad);
  background:var(--white);border:1px solid var(--line);cursor:pointer;overflow:hidden;
  touch-action:manipulation}
/* The cap is taken off BOTH ends of a centred box, so half of it lands under
   the artwork. Subtracting the padding first makes the clearance measure from
   the border box, which is what the glyph is positioned against — so it holds
   at every width even though --work-pad shrinks with the viewport. The height
   available to the artwork works out constant either way, 4px less than the old
   asymmetric reserve gave it. */
/* The work hangs on the panel rather than sitting flat on it. Two stacked
   drop-shadows, the way a lit wall actually behaves:
     · a tight, dark contact shadow immediately under the frame;
     · a broad, soft ambient one below and around it.
   `drop-shadow` (not `box-shadow`) because it follows the IMAGE'S ALPHA, not
   its element box — these photographs are the artwork on its own ground, so
   the shadow traces the work's real silhouette.

   Sized to stay inside the frame: `.work-media` is `overflow:hidden`, so a
   shadow bigger than the padding gets cut off square, which looks like a
   printing error. The largest here reaches about 14 + 22/2 = 25px below the
   work, against the 38px of clearance the glyph reserve already guarantees
   beneath it. Change the offsets and check that sum. */
.work-media img{width:100%;height:100%;
  max-height:calc(100% - 2 * (var(--work-glyph-clear) - var(--work-pad)));
  object-fit:contain;
  filter:drop-shadow(0 1px 2px rgba(28,26,23,.16))
         drop-shadow(0 14px 22px rgba(28,26,23,.20))}
/* Tucked to 6px/28px so --work-glyph-clear can stay small. Change either and
   the custom property above must change with it. */
/* Crop-mark badge. The box grew 28px -> 30px when the glyph became a drawn
   14px mark: the mark needs air around it or the two hairlines (badge border,
   icon) sit on top of each other and read as a smudge. */
.work-zoom{position:absolute;bottom:6px;right:6px;width:30px;height:30px;display:flex;align-items:center;justify-content:center;color:var(--muted);background:transparent;border:1px solid var(--line);opacity:1;transition:opacity .2s,color .3s cubic-bezier(.22,.61,.36,1),border-color .3s cubic-bezier(.22,.61,.36,1)}
/* Touch devices have no hover, so the glyph is the ONLY thing telling a visitor
   the artwork opens — it stays visible there. It hides on pointer devices until
   the cursor arrives, where hover does that job. (This matters more now the
   per-card "Enquire" link is gone: the image is the sole way into a work.) */
@media (hover:hover){
  .work-zoom{opacity:0}
  .work-media:hover .work-zoom,.work-media:focus-visible .work-zoom{opacity:1}
  /* The work leans in. 1.03 over nearly a second — slow enough that you notice
     the artwork rather than the effect, and small enough that it stays inside
     its mount (the frame is not clipped, so the drop-shadow under the work is
     never cut off at the border). The shadow deepens with it, as if the piece
     had come a little off the wall. */
  .work-media img{transition:transform .9s cubic-bezier(.22,.61,.36,1),
                             filter .55s cubic-bezier(.22,.61,.36,1)}
  .work-media:hover img{transform:scale(1.03);
    filter:drop-shadow(0 2px 3px rgba(28,26,23,.18))
           drop-shadow(0 20px 30px rgba(28,26,23,.24))}
  .work-media:hover .work-zoom{color:var(--gold);border-color:var(--gold)}
}
@media (prefers-reduced-motion: reduce){
  .work-media img,.work-media:hover img{transition:none;transform:none}
}
.work-media:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* Was a flat 1.5rem, so on a 390px phone it rendered at 27px — bigger than
   the FAQ questions and level with the "Why Banksy?" subhead. It scales now,
   like every other heading on the page. */
.work-title{font-family:var(--body);font-size:clamp(1.2rem,2.6vw,1.5rem);font-weight:400;font-style:italic;line-height:1.18;margin:1.2rem 0 .3rem}
/* The title opens the artwork, same as the image. Reset the button to plain
   text; underline on hover only, via text-decoration rather than a positioned
   pseudo-element, because these titles wrap onto two lines. */
.work-title-link{appearance:none;-webkit-appearance:none;background:none;border:0;padding:0;margin:0;
  font:inherit;color:inherit;text-align:left;cursor:pointer;text-decoration:none;
  text-underline-offset:.16em;text-decoration-thickness:1px}
.work-title-link:hover,.work-title-link:focus-visible{text-decoration:underline}
/* No hover on a touch screen, so the underline is permanent there — otherwise
   nothing tells a visitor the title opens the work. Same reasoning as
   .work-zoom, which stays visible on touch for exactly this reason. */
@media (hover:none){
  .work-title-link{text-decoration:underline;text-decoration-color:var(--line)}
}
.work-title-link:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
.work-meta{font-family:var(--label);font-size:.7rem;letter-spacing:.06em;color:var(--muted);text-transform:uppercase;margin:0 0 .7rem;line-height:1.5}
.work-desc{color:var(--muted);font-size:.96rem;margin:0 0 1rem}
.work-year{font-family:var(--label);font-size:var(--fs-label-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin:.35rem 0 0}
/* CATALOGUE ALIGNMENT. Once the grid has more than one column, a title that
   wraps to two lines ("Heavy Weaponry — London, New York, Bristol") pushed its
   year below the years beside it, so the metadata staggered across the row. In
   a printed catalogue that line is always level. Reserving two lines of title
   costs a card with a short title some white space and buys the row its
   baseline — the right trade at this price point.
   2.36em = 2 x the title's own 1.18 line-height. If either changes, change
   this with them. Single column (phones) keeps the tighter natural height. */
@media(min-width:720px){
  .work-title{min-height:2.36em}
}
/* Centred under the works grid. Reads as "the next step", not as a way out. */
.more-works{margin:clamp(2.6rem,5vw,3.6rem) 0 0;text-align:center}
.more-works a{display:inline-block;font-family:var(--label);font-size:var(--fs-label-sm);
  letter-spacing:.15em;text-transform:uppercase;color:var(--ink);
  border-bottom:1px solid var(--line);padding-bottom:5px;
  transition:border-color .35s cubic-bezier(.22,.61,.36,1)}
.more-works a span[aria-hidden]{display:inline-block;font-size:.92em;margin-left:.15em;
  transition:transform .35s cubic-bezier(.22,.61,.36,1)}
@media (hover:hover){
  .more-works a:hover{border-color:var(--ink)}
  .more-works a:hover span[aria-hidden]{transform:translate(2px,-2px)}
}
.more-works a:focus-visible{outline:2px solid var(--accent);outline-offset:4px}

.sourcing-cta{max-width:680px;margin:clamp(3rem,6vw,4.5rem) auto 0;padding-top:clamp(2.4rem,5vw,3.4rem);border-top:1px solid var(--line);text-align:center}
/* Centred text only *reads* centred when the lines are close in length. Both
   blocks were breaking raggedly — the heading left "by Banksy?" alone on line
   two — which looks like a misaligned block even though every element is
   mathematically on the page axis. `balance` evens the heading, `pretty` stops
   the paragraph orphaning its last word. */
.sourcing-cta h3{font-family:var(--display);font-size:clamp(1.5rem,2.8vw,2rem);font-weight:400;
  letter-spacing:-.01em;margin:0 0 .8rem;text-wrap:balance}
.sourcing-cta p{color:var(--muted);max-width:56ch;margin:0 auto 1.8rem;text-wrap:pretty}
/* MOBILE: a SQUARE frame, not a 4/3 landscape one.
   `object-fit:contain` never crops, so nothing was ever truly cut off — but a
   portrait work in a landscape frame is scaled to fit the short side, which
   left it small and high in the box with a deep empty strip beneath it. That
   reads as truncation. A square frame is 42% taller for the same width, so an
   upright work comes out 302px instead of 212px on a 390px screen, and a
   landscape one still fills the width.

   NOT `aspect-ratio:auto` with `height:auto`: that shows the work at its own
   proportions but reserves NO space until the image loads, so every card jumps
   as it arrives. A fixed ratio keeps the reservation and the layout stable. */
/* MOBILE: no frame, no glyph, nothing cropped.
   The bordered box forced every work into the same rectangle, so a row of
   different-shaped paintings read as a row of uneven boxes with the artwork
   floating somewhere inside each one. Dropped: the border, the panel
   background, the padding and the expand glyph. What is left is the artwork
   itself at its own proportions, held off the page by its drop shadow.

   `aspect-ratio: auto 3/4` on the image is what keeps this from causing layout
   shift: the `auto` keyword means "use the image's real proportions once it has
   loaded" and the 3/4 is only the placeholder used before that — so space is
   reserved up front AND the work is shown whole. A bare `height:auto` would
   reserve nothing and make every card jump as its image arrived. */
@media(max-width:719px){
  .work-media{aspect-ratio:auto;padding:0;border:0;background:transparent;overflow:visible}
  /* max-height:none — the desktop cap reserves room for the expand glyph, and
     there is no glyph here. A percentage max-height against an auto-height
     parent is undefined territory anyway. */
  /* No frame here to clip against, so the ambient shadow can sit deeper and
     do the whole job of lifting the work off the page. */
  .work-media img{height:auto;max-height:none;aspect-ratio:auto 3/4;
    filter:drop-shadow(0 1px 2px rgba(28,26,23,.14))
           drop-shadow(0 18px 30px rgba(28,26,23,.18))}
  .work-zoom{display:none}
}
@media(min-width:720px){.works{grid-template-columns:1fr 1fr}}
@media(min-width:1000px){.works{grid-template-columns:repeat(3,1fr)}}

/* ---------------------------- Why ---------------------------------- */
.why{padding-block:var(--pad-block)}
/* This section carries the most in one place — question, intro, two columns,
   a film with its caption, a CTA and the press band. It read as squashed, so
   the fix is air between the parts, not smaller type. */
.why-grid{display:flex;flex-direction:column;gap:2.8rem;margin-top:clamp(.5rem,2vw,1.6rem)}
.why-copy h3{font-family:var(--display);font-size:clamp(1.5rem,2.8vw,2rem);font-weight:400;
  letter-spacing:-.012em;line-height:1.16;margin:0 0 .75em}
.why-copy p{color:var(--muted);max-width:54ch;margin:0;line-height:1.78}
.why-copy p + p{margin-top:1.5rem}

/* Film. Framed and shadowed like .hero-media so the two read as one family
   rather than "hero" and "a video someone pasted in". */
.why-media{margin:0;display:flex;flex-direction:column;align-items:flex-start}
.why-media .video-embed,.why-media .media-caption{width:100%}
.video-embed{position:relative;width:100%;aspect-ratio:16/9;background:var(--ink);
  border:1px solid var(--line);box-shadow:0 22px 54px -34px rgba(28,26,23,.34)}
.video-embed iframe{position:absolute;inset:0;width:100%;height:100%;border:0}

/* ---- The facade -------------------------------------------------------
   Same frame and shadow as .video-embed, because it stands in the same place
   — but it is a button, not an embed: nothing third-party loads until it is
   pressed. The poster is a background-image so a missing or slow one leaves a
   composed dark panel rather than a broken <img>. */
/* The noscript fallback renders the old inline embed; the facade is hidden in
   that case only because it is never scripted. Nothing to do here — noted so
   the pair is not mistaken for duplication. */
.video-facade{position:relative;display:block;width:100%;aspect-ratio:16/9;
  padding:0;background:#17161a;border:1px solid var(--line);
  box-shadow:0 22px 54px -34px rgba(28,26,23,.34);
  cursor:pointer;overflow:hidden;
  -webkit-tap-highlight-color:transparent;touch-action:manipulation}
.video-facade-img{position:absolute;inset:0;background-size:cover;background-position:center;
  /* A hint of a scrim, so a white play mark holds on a bright frame. It is
     lifted on hover rather than removed, which is the whole "the film comes
     forward when you approach it" move. */
  transition:transform .7s cubic-bezier(.22,.61,.36,1),opacity .4s ease}
.video-facade-img::after{content:"";position:absolute;inset:0;
  background:linear-gradient(180deg,rgba(0,0,0,.10),rgba(0,0,0,.28))}
/* The play mark. Gold, like every other primary action, and a solid triangle
   rather than a hairline one — this is the one glyph on the site that has to
   read instantly at arm's length. */
.video-play{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  display:flex;align-items:center;justify-content:center;
  width:clamp(58px,9vw,74px);height:clamp(58px,9vw,74px);border-radius:50%;
  background:var(--gold);color:var(--white);
  box-shadow:0 10px 30px -12px rgba(0,0,0,.6);
  transition:transform .45s cubic-bezier(.22,.61,.36,1),background .35s ease}
.video-play svg{margin-left:3px}   /* optical centring: a triangle is left-heavy */
.video-clock{position:absolute;right:10px;bottom:10px;
  font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.1em;
  color:var(--white);background:rgba(0,0,0,.55);padding:.28rem .5rem;border-radius:2px}
@media (hover:hover){
  .video-facade:hover .video-facade-img{transform:scale(1.03)}
  .video-facade:hover .video-play{transform:translate(-50%,-50%) scale(1.06);background:var(--gold-deep)}
}
.video-facade:focus-visible{outline:2px solid var(--gold);outline-offset:3px}
@media (prefers-reduced-motion: reduce){
  .video-facade-img,.video-play{transition:none}
  .video-facade:hover .video-facade-img{transform:none}
}

/* ---- The film, full screen -------------------------------------------
   Deliberately the same language as the artwork stage: near-black ground, one
   slim bar, nothing else. The dialog is sized to the FILM (16:9 capped by both
   viewport axes) rather than to the screen, so the player is never letterboxed
   inside a box that is itself letterboxed. */
.video-lightbox{position:fixed;inset:0;z-index:210;display:flex;
  align-items:center;justify-content:center;padding:clamp(10px,2.5vw,28px);
  opacity:0;transition:opacity .32s cubic-bezier(.22,.61,.36,1)}
.video-lightbox[hidden]{display:none}
.video-lightbox.is-open{opacity:1}
.vlb-overlay{position:absolute;inset:0;background:rgba(12,11,13,.94);
  backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px)}
.vlb-dialog{position:relative;z-index:1;width:min(100%,calc((100svh - 96px) * 16 / 9));
  display:flex;flex-direction:column;gap:.6rem;
  transform:translateY(12px) scale(.99);
  transition:transform .38s cubic-bezier(.22,.61,.36,1)}
.video-lightbox.is-open .vlb-dialog{transform:none}
.vlb-bar{display:flex;align-items:center;justify-content:space-between;gap:1rem}
.vlb-title{margin:0;font-family:var(--label);font-size:var(--fs-label-xs);
  letter-spacing:.2em;text-transform:uppercase;color:rgba(255,255,255,.62);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* Same 40px tool as the artwork stage, so "close" is one object across the
   whole site rather than a different button per overlay. */
.vlb-close{width:40px;height:40px;flex:0 0 auto;display:inline-flex;
  align-items:center;justify-content:center;
  color:rgba(255,255,255,.92);background:transparent;
  border:1px solid rgba(255,255,255,.22);border-radius:3px;cursor:pointer;
  touch-action:manipulation;transition:background .25s,color .25s,border-color .25s}
@media (hover:hover){ .vlb-close:hover{background:rgba(255,255,255,.95);color:#111013;border-color:transparent} }
.vlb-close:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
.vlb-frame{position:relative;width:100%;aspect-ratio:16/9;background:#000;overflow:hidden}
.vlb-frame iframe{position:absolute;inset:0;width:100%;height:100%;border:0}
@media (prefers-reduced-motion: reduce){
  .video-lightbox,.vlb-dialog{transition:none}
}
/* An unlabelled embed is a black rectangle: nothing says what it is or how
   long it runs, so it reads as decoration and goes unplayed. The caption also
   gives the right column a bottom edge, which is what stops the old layout
   trailing off into empty space beside the text. */
.media-caption{display:flex;flex-direction:column;gap:.3rem;
  margin-top:1.05rem;padding-top:.95rem;border-top:1px solid var(--line)}
.media-kicker{font-family:var(--label);font-size:.58rem;letter-spacing:.24em;
  text-transform:uppercase;color:var(--accent)}
.media-title{font-family:var(--display);font-size:1.18rem;font-weight:500;line-height:1.3;color:var(--ink)}
.media-meta{font-family:var(--label);font-size:.62rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted)}
/* Sits under the film: having watched it, the next step is the works. */
.why-cta{margin-top:1.5rem;align-self:flex-start}

/* Press band — closes the section on third-party authority. Same typographic
   language as the credentials band under the hero, so the page bookends its
   two proof points. */
/* The press band sat ~83px below the grid and read as tacked on. It is a
   separate claim from the gallery copy and needs to look like one. */
.press{margin-top:clamp(4rem,8vw,0.5rem);padding-top:clamp(2.6rem,5vw,1.6rem);
  border-top:1px solid var(--line);text-align:center}
.press-label{font-family:var(--label);font-size:.6rem;letter-spacing:.24em;line-height:1.7;
  text-transform:uppercase;color:var(--muted);margin:0 auto 1.9rem;max-width:40ch;text-wrap:balance}
/* Eight names on a wrapped flex row broke 6 + 2, which left the two names that
   can actually be evidenced stranded on a line of their own looking like an
   afterthought. A MEASURE governs the break now, not the gap, so the row splits
   4 + 4.

   Middot separators were tried and removed: a flex row cannot know where it
   wrapped, so the separator before the first item of the second line hangs at
   the start of that line and reads as a typo. Space does the separating. */
.press-list{list-style:none;margin:0 auto;padding:0;
  display:grid;grid-template-columns:repeat(2,auto);justify-content:center;justify-items:center;
  align-items:baseline;gap:.45rem clamp(1.4rem,3vw,2.4rem)}
@media(min-width:720px){
  /* A GRID, not a wrapped row. A measure could not be tuned to break 4 + 4
     reliably — the names are different widths, so any width that split them
     evenly at one viewport split them 5 + 3 at the next. Four explicit
     columns cannot drift, and the shared column widths line the two rows up
     with each other, which reads as a considered list rather than a sentence
     that happened to wrap. */
  .press-list{grid-template-columns:repeat(4,auto);gap:1rem clamp(1.6rem,3vw,2.8rem)}
}
/* Belt and braces: Safari can still paint a marker on a flex-item <li> even
   with list-style:none on the <ul>, which showed as a stray bullet above
   "Associated Press". Kill it on the item and on ::marker. */
.press-list li{list-style:none}
.press-list li::marker{content:"";font-size:0}
.press-list li{font-family:var(--display);font-size:clamp(1.02rem,1.6vw,1.26rem);font-weight:500;
  letter-spacing:.012em;color:var(--ink);opacity:.8;white-space:nowrap;transition:opacity .4s ease}
/* Hover lifts the name under the cursor to full weight. Deliberately NOT the
   "dim every sibling" trick: these are plain text, not links, and dimming them
   drops the contrast of legible copy below AA for the sake of an effect. */
@media (hover:hover) and (min-width:720px){
  .press-list li:hover{opacity:1}
}
@media (prefers-reduced-motion: reduce){ .press-list li{transition:none} }

/* ---- The one verified citation, under the list of names ---------------
   Set QUIETER than the testimonials, deliberately. Those are collectors
   speaking about the gallery and they carry the section; this is the
   director quoted in a newspaper, and it is here to make the six names above
   it checkable rather than to be another voice competing for attention.
   Hence: display face but small, no quotation-mark ornament, and the
   attribution in the label font like every other caption on the page. */
/* One quote centres in a single column. Two sit side by side from 860px —
   two 44ch measures stacked would push the exhibition timeline a full screen
   down the page for no gain. .is-pair is set by the template from the count,
   so adding or removing a quotation needs no change here. */
/* The band used to read as three stacked objects — a label, a list, then two
   quotes marooned in a wide gutter. It is one composition now: a hairline
   under the names binds them to the quotations, the pair sits in an even
   two-column grid divided by a rule rather than by empty space, and the
   captions are pinned to the BOTTOM of each column so they line up with each
   other however many lines the quotations run to. */
.press-quotes{display:grid;grid-template-columns:1fr;gap:clamp(1.8rem,3.5vw,2.4rem);
  margin-top:clamp(1.3rem,2.6vw,1.9rem);padding-top:clamp(1.6rem,3vw,2.2rem);
  border-top:1px solid var(--line)}
@media(min-width:860px){
  .press-quotes.is-pair{grid-template-columns:1fr 1fr;gap:0;align-items:stretch}
  /* The divider is the second column's own border, so it can never be a
     stray element left behind when there is only one quotation. */
  .press-quotes.is-pair .press-quote + .press-quote{border-left:1px solid var(--line)}
  .press-quotes.is-pair .press-quote{max-width:none;padding-inline:clamp(1.6rem,3.4vw,3rem);
    display:flex;flex-direction:column}
  /* The blockquote absorbs the slack, so both captions START at the same
     height and the two "ACORIS ANDIPA" lines sit on one line across the band.
     Pinning the captions to the bottom instead would align their last lines,
     which is not the line anyone reads. */
  .press-quotes.is-pair blockquote{max-width:34ch;margin-inline:auto;flex:1 1 auto}
  /* One credit runs to two lines and the other to one. Without a floor the
     captions are different heights, so even with the blockquotes absorbing the
     slack the two "ACORIS ANDIPA" lines land 17px apart — close enough to look
     like a mistake rather than a decision. Two lines of the label face is the
     floor; a third would only be needed if a source line ever got longer. */
  .press-quotes.is-pair .press-quote-src{min-height:3em}
}
.press-quote{margin:0 auto;max-width:44ch;text-align:center}
.press-quote blockquote{margin:0;font-family:var(--display);font-weight:400;
  font-size:clamp(1.02rem,1.7vw,1.22rem);line-height:1.5;color:var(--ink);text-wrap:balance}
.press-quote figcaption{padding-top:.9rem;display:flex;flex-direction:column;gap:.15rem}
.press-quote-name{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.2em;
  text-transform:uppercase;color:var(--ink)}
/* balance + the non-breaking space in the template keep "March 2026" whole:
   an orphaned year on its own line reads as a mistake in a credit. */
.press-quote-src{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.08em;
  color:var(--muted);line-height:1.5;text-wrap:balance;max-width:38ch;margin:0 auto}
.press-quote-src a{border-bottom:1px solid var(--line)}

/* The director's quotation under the film. Left-aligned to the media column
   and set on a hairline rather than centred like the press band: it is a
   caption's continuation, not a display quote, and centring it would compete
   with the section head above. Measure held to 46ch so it never runs wider
   than the film it sits under on a big screen. */
.film-quote{margin:1.15rem 0 0;padding-left:1rem;border-left:1px solid var(--line);max-width:46ch}
.film-quote blockquote{margin:0;font-family:var(--display);font-weight:400;
  font-size:clamp(.98rem,1.5vw,1.14rem);line-height:1.55;color:var(--ink);text-wrap:pretty}
.film-quote figcaption{margin-top:.5rem;font-family:var(--label);font-size:var(--fs-label-xs);
  letter-spacing:.08em;color:var(--muted);line-height:1.5}
.film-quote figcaption a{border-bottom:1px solid var(--line);transition:border-color .3s ease,color .3s ease}
@media (hover:hover){ .film-quote figcaption a:hover{color:var(--gold);border-bottom-color:var(--gold)} }
@media (hover:hover){ .press-quote-src a:hover{border-bottom-color:var(--ink)} }

/* Stacked (phone/tablet) the film moves ABOVE the gallery copy. Left in DOM
   order it landed below the CTA, so anyone who tapped "Start a Conversation"
   never saw it — and it gave the long copy block no visual break. */
@media(max-width:999px){
  .why-media{order:-1}
  /* Stacked, the press names follow the FILM rather than closing the section.
     They are the same claim the film makes — this is who the director is —
     and read as its footnote there; a screen further down they were just the
     end of a long block of copy. Tighter than the desktop band, because they
     are no longer separating two halves of the section. */
  .press{order:-1;margin-top:clamp(2rem,6vw,2.8rem);padding-top:clamp(1.7rem,4vw,2.3rem)}
}
@media(min-width:1000px){
  /* wrap + a full-width third item is what keeps the press band spanning both
     columns now that it lives INSIDE the grid (it has to, so `order` can move
     it under the film on a phone). Two columns on the first line, the band on
     the second — the same layout as before, from one extra rule. */
  .why-grid{flex-direction:row;flex-wrap:wrap;align-items:flex-start;gap:clamp(3.5rem,6vw,5.5rem)}
  .press{flex:0 0 100%;order:1}
  .why-copy{flex:1 1 auto;min-width:0}
  /* NOT sticky. It used to hold in view while the copy scrolled past it, which
     was near-invisible while the two columns were the same height — but once
     the press band moved inside the grid the media column became the shorter
     one, the sticky finally had travel, and the film sat there following the
     visitor down the page. A picture that will not stay where it was put is
     the opposite of a gallery wall. It scrolls with everything else. */
  .why-media{flex:0 0 46%;max-width:530px}
}

/* -------------------------- Legacy timeline ------------------------ */
.legacy{padding-block:var(--pad-block);background:var(--white);border-block:1px solid var(--line)}
/* The exhibition intro runs the full wrap, not the 620px column every other
   section intro uses. It is the longest intro on the page and it sits above a
   full-width filmstrip, so the narrow measure left a tall, thin block of text
   over a wide object. Scoped to .legacy on purpose — every other
   .section-intro keeps its 620px. (0,2,0 beats both the base rule and the
   880px short-desktop override, so this wins wherever it sits in the file.) */
.legacy .section-intro{max-width:none}

/* MOBILE-FIRST: vertical timeline (dot + entry down a left-hand line).
   The year sits in its own narrow column ON THE SAME LINE as the title rather
   than stacked above it. Thirteen entries stacked three-deep ran to 1705px of
   phone scroll; as two-line entries the same list is ~1000px. */
.timeline{display:flex;flex-direction:column;margin-top:1.4rem}
.tl-item{
  display:grid;grid-template-columns:auto 4.1rem 1fr;
  grid-template-areas:"dot year title" ".   .    place";
  align-items:baseline;
  column-gap:.8rem;padding-bottom:1.35rem;position:relative;
}
.tl-item::before{content:"";position:absolute;left:5px;top:.5rem;height:100%;width:1.5px;background:var(--line)}
.tl-item:last-child::before{display:none}
.tl-dot{grid-area:dot;align-self:start;position:relative;top:.45rem;width:11px;height:11px;
  border-radius:50%;border:1.5px solid var(--ink);background:var(--white);z-index:1}
.tl-year{grid-area:year;font-family:var(--label);font-size:var(--fs-label-sm);letter-spacing:.04em;
  color:var(--ink);white-space:nowrap}
.tl-title{grid-area:title;font-family:var(--display);font-size:1.08rem;font-weight:600;
  font-style:italic;line-height:1.32;margin-bottom:.2rem}
.tl-place{grid-area:place;color:var(--muted);font-size:.88rem;line-height:1.5}

/* Controls bar + edge fades are wide-screen only (mobile is a plain vertical list). */
.timeline-bar{display:none}
.timeline-frame{position:relative}

/* TABLET / DESKTOP: horizontal "filmstrip" on one line; roomy columns, scrolls sideways.
   The old version put overflow on the <ol> itself inside .wrap, with no fade, no
   controls and a hidden text hint below. The last card was sliced mid-word at the
   container edge, which reads as a broken layout rather than as scrollable content.
   Everything here exists to fix that impression. */
@media(min-width:720px){
  /* The bar held the count on the left and the arrows on the right. With the
     arrows gone (dots below the strip do that job), the count centres. */
  .timeline-bar{
    display:flex;align-items:center;justify-content:center;gap:1.5rem;
    margin-top:.4rem;padding-bottom:1.1rem;
  }
  /* States the sweep in one line, so the credential lands even if nobody scrolls. */
  .timeline-count{font-family:var(--label);font-size:.62rem;letter-spacing:.2em;
    text-transform:uppercase;color:var(--muted);margin:0;
    /* Nudged up without changing the layout — position/top, not margin, so the
       bar keeps its own height and the strip below does not move with it.
       Inside the >=720px block because the count only exists there; below that
       .timeline-bar is display:none. */
    position:relative;top:-50px}

  /* Bleeds out to the wrap's outer edge so cards run past the text column rather
     than stopping dead in line with it. */
  .timeline-scroller{
    margin-inline:calc(-1 * clamp(20px,5vw,48px));
    padding-inline:clamp(20px,5vw,48px);
    overflow-x:auto;overscroll-behavior-x:contain;
    scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;
    /* Without this the snap point ignores the padding and the browser scrolls
       itself 48px on load, so the strip opens already "used" and the Previous
       button lights up before anyone has touched it. */
    scroll-padding-inline:clamp(20px,5vw,48px);
    scrollbar-width:none;
    /* overflow-y stays `auto` (setting overflow-x already computes it from
       `visible` to `auto`). Do NOT "fix" it to hidden or clip: in Chromium that
       stops the box accepting a horizontal wheel gesture entirely, so the strip
       can no longer be scrolled with a trackpad.
       The rule that keeps this safe is instead: NOTHING INSIDE MAY OVERFLOW
       VERTICALLY. With `auto`, even 1px of vertical overflow turns this into a
       vertical scroll container, and then parking the wheel over it stalls the
       page while those pixels scroll first — the "page gets stuck on the
       timeline" bug. See the reveal-transform override near the bottom of this
       file for what caused it. Measured scrollHeight === clientHeight at 1400,
       1024 and 760px; keep it that way. */
  }
  .timeline-scroller::-webkit-scrollbar{display:none}
  .timeline-scroller:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
  /* Same grab-and-drag affordance as the testimonial carousel. Pointer devices
     only: touch already drags natively and must keep its own momentum. */
  @media (pointer:fine){
    .timeline-scroller{cursor:grab}
    .timeline-scroller.is-dragging{cursor:grabbing;user-select:none;-webkit-user-select:none}
    .timeline-scroller.is-dragging *{cursor:grabbing}
  }

  /* Edge fades: the visual cue that content continues past the edge. Default OFF
     so a no-JS page never has a permanent veil over the first card; main.js adds
     .can-left / .can-right from the actual scroll position. */
  .timeline-frame::before,.timeline-frame::after{
    content:"";position:absolute;top:0;bottom:0;width:clamp(52px,7vw,104px);
    pointer-events:none;z-index:2;opacity:0;transition:opacity .35s ease}
  .timeline-frame::before{left:calc(-1 * clamp(20px,5vw,48px));
    background:linear-gradient(to right,var(--white) 18%,rgba(255,254,251,0))}
  .timeline-frame::after{right:calc(-1 * clamp(20px,5vw,48px));
    background:linear-gradient(to left,var(--white) 18%,rgba(255,254,251,0))}
  .timeline-frame.can-left::before,.timeline-frame.can-right::after{opacity:1}

  .timeline{
    flex-direction:row;flex-wrap:nowrap;align-items:stretch;margin-top:0;
    padding:12px 0 .4rem;   /* top space so dots aren't clipped by the scroll box */
  }
  .tl-item{
    display:flex;flex-direction:column;position:relative;scroll-snap-align:start;
    flex:0 0 clamp(224px,21vw,268px);
    padding:2.3rem 2.6rem 0 0;border-top:1px solid var(--line);
  }
  .tl-item::before{display:none}
  .tl-dot{position:absolute;top:-6px;left:0}
  .tl-year{display:block;margin-bottom:.55rem}
  /* Reserve two lines of title so the "place" lines start on a shared baseline
     across the strip — ragged place lines were most of why it looked untidy.
     Two, not three: only one of the thirteen titles runs to three lines, so
     reserving for that one would put a blank line under the other twelve. */
  .tl-title{display:block;font-size:1.14rem;margin-bottom:.5rem;min-height:2.6em}
  /* The venue is the credential — Palazzo Ducale carries more weight with a
     collector than the show's title does. Lift it out of the fine print. */
  .tl-place{display:block;font-size:.92rem;color:var(--muted);transition:color .35s ease}
  /* Hovering an entry brings it forward: the dot fills, the venue darkens.
     Thirteen identical cards gave the cursor nothing to hold on to. */
  .tl-item{transition:opacity .35s ease}
  .tl-dot{transition:background .35s cubic-bezier(.22,.61,.36,1),transform .35s cubic-bezier(.22,.61,.36,1)}
}
@media (hover:hover) and (min-width:720px){
  .tl-item:hover .tl-dot{background:var(--ink);transform:scale(1.25)}
  .tl-item:hover .tl-place{color:var(--ink)}
  .tl-item:hover .tl-year{color:var(--accent)}
}

/* Dots. One per exhibition, below the strip — they show position AND extent at
   a glance, and each jumps to its own entry. They replaced prev/next arrows,
   which could only step and gave no sense of how many there were. */
/* SHARED dot component — the exhibition timeline and the testimonials both use
   it, so they behave and look identical. Add .dots/.dot-btn to anything else
   that needs position indicators rather than writing a second set. */

/* ------------------------- Reference notes -------------------------
   Two blocks share this: "How to read an original" under the collection,
   and "Buying from the gallery" before the FAQ. Same idiom as the
   bibliography — small-caps term, one sentence, hairline rule — so the
   page has ONE voice for explanatory material instead of inventing a new
   card style each time something needs explaining.
   Two columns from 760px, four would make each measure too narrow to read. */
.notes{margin-top:clamp(2.6rem,5vw,4rem)}
.notes-head{font-family:var(--display);font-weight:400;text-align:center;
  font-size:clamp(1.28rem,2.2vw,1.7rem);margin:0 0 .7rem}
.notes-intro{max-width:56ch;margin:0 auto clamp(1.8rem,3.5vw,2.6rem);text-align:center;
  color:var(--muted);font-size:.92rem;line-height:1.7}
.notes-grid{display:grid;grid-template-columns:1fr;gap:clamp(1.6rem,3vw,2.2rem);margin:0}
.note{min-width:0}
.note-term{font-family:var(--label);font-weight:400;font-size:var(--fs-label-xs);
  letter-spacing:.2em;text-transform:uppercase;color:var(--muted);line-height:1.6;
  margin:0 0 .9rem;padding-bottom:.7rem;border-bottom:1px solid var(--line)}
.note-body{margin:0;color:var(--muted);font-size:.93rem;line-height:1.72;max-width:52ch}
@media(min-width:760px){
  .notes-grid{grid-template-columns:1fr 1fr;gap:clamp(1.8rem,3.5vw,2.8rem) clamp(2rem,4vw,3.4rem)}
}
/* Three across from 900px — for three items, two columns leaves an orphan. */
@media(min-width:900px){
  .notes-grid--3{grid-template-columns:repeat(3,1fr);gap:clamp(1.8rem,3.5vw,3rem)}
}

/* ------------------------ Further reading --------------------------
   Same reference idiom again: hairline over each entry, quiet meta line,
   one sentence. The TITLE is the link and the only ink-coloured text in
   the block, so the eye goes straight to what each piece is about rather
   than to three identical "read more" tails. */
.reading{padding-block:0 var(--pad-block)}
.reading .notes{margin-top:0;padding-top:clamp(2.4rem,4.5vw,3.6rem)}
.read{min-width:0;border-top:1px solid var(--line);padding-top:.95rem}
.read-title{font-family:var(--display);font-weight:600;font-size:1.04rem;line-height:1.35;
  margin:0 0 .38rem;color:var(--ink)}
.read-title a{color:inherit;text-decoration:underline;text-decoration-thickness:1px;
  text-underline-offset:.2em;text-decoration-color:var(--line);
  transition:color .3s ease,text-decoration-color .3s ease}
.read-title a:hover,.read-title a:focus-visible{color:var(--gold);text-decoration-color:var(--gold)}
.read-meta{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.14em;
  text-transform:uppercase;color:var(--muted);margin:0 0 .55rem}
.read-note{margin:0;color:var(--muted);font-size:.9rem;line-height:1.7;max-width:46ch}

/* The standalone band. No border-block: it sits between two sections that
   already have their own, and a third rule in a row reads as a stack of
   boxes. Its own top margin is what separates it. */
.sale-note{padding-block:0 var(--pad-block)}
.sale-note .notes{margin-top:0;padding-top:clamp(2.6rem,5vw,4rem)}


/* --------------------------- WhatsApp routes -----------------------
   Two places: under the enquiry form, and in the footer contact column.
   Both are deliberately UNDERWEIGHTED. Gold and button weight belong to
   Send Enquiry; if this looked equally important it would split the
   decision at the one moment the page cannot afford to. It is a second
   door to the same room, signposted quietly. */
.form-alt{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:.1rem .55rem;margin:1.1rem 0 0;text-align:center}
.form-alt-label{color:var(--muted);font-size:.86rem}
.form-alt-link{display:inline-flex;align-items:center;gap:.42rem;
  font-family:var(--label);font-size:var(--fs-label-sm);letter-spacing:.09em;
  text-transform:uppercase;color:var(--ink);
  border-bottom:1px solid var(--line);padding-bottom:2px;
  transition:color .3s ease,border-color .3s ease}
.form-alt-link:hover,.form-alt-link:focus-visible{color:var(--gold);border-color:var(--gold)}
/* The bubble is a 1px hairline like every other icon; it needs a hair of
   optical lift to sit on the cap height of uppercase label type. */
.form-alt-link .icon,.footer-wa .icon{position:relative;top:-1px}
.footer-wa{display:inline-flex;align-items:center;gap:.45rem}


/* ------------------------ Bibliography (legacy) --------------------
   Sits under the exhibition timeline. Every decision here is aimed at
   "reference block", not "marketing block": no rules, no boxes, no bold.
   The titles are the only italic display type; publisher and ISBN drop to
   the label face at label sizes so a column scans top-down in one pass.
   Mobile-first single column; three columns from 900px. */
.biblio{margin-top:clamp(2.6rem,5vw,4rem)}
.biblio-head{font-family:var(--display);font-weight:400;text-align:center;
  font-size:clamp(1.28rem,2.2vw,1.7rem);margin:0 0 .7rem}
.biblio-intro{max-width:60ch;margin:0 auto clamp(1.8rem,3.5vw,2.6rem);text-align:center;
  color:var(--muted);font-size:.92rem;line-height:1.7}
.biblio-grid{display:grid;grid-template-columns:1fr;gap:clamp(1.8rem,3.5vw,2.6rem)}
.biblio-col{min-width:0}
/* The group label is the only uppercase text in the block, so it has to do
   the whole job of separating three lists that otherwise look alike. */
.biblio-group{font-family:var(--label);font-weight:400;font-size:var(--fs-label-xs);
  letter-spacing:.2em;text-transform:uppercase;color:var(--muted);line-height:1.6;
  margin:0 0 1rem;padding-bottom:.7rem;border-bottom:1px solid var(--line)}
.biblio-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:.95rem}
.biblio-item{display:flex;flex-direction:column;gap:.22rem}
.biblio-title{font-family:var(--display);font-size:1.04rem;font-weight:600;font-style:italic;
  line-height:1.35;color:var(--ink);display:block}
/* Every title is a link to its catalogue record, and all eight open in a NEW
   TAB — that is what makes linking safe here. A visitor who verifies a title
   still has the landing page open behind them, so the block gains one-click
   proof without becoming eight exits from a paid page.
   No ↗ marks: eight arrows in one block turns a reference list into a
   navigation menu. The .biblio-intro line says the titles link out, and each
   anchor carries an sr-only note for anyone not reading it visually. */
.biblio-title a{color:inherit;text-decoration:underline;text-decoration-thickness:1px;
  text-underline-offset:.2em;text-decoration-color:var(--line);
  transition:color .3s ease,text-decoration-color .3s ease}
.biblio-title a:hover,.biblio-title a:focus-visible{color:var(--gold);text-decoration-color:var(--gold)}
.biblio-meta{color:var(--muted);font-size:.84rem;line-height:1.5}
/* The ISBN is the whole point of the block — it is what makes the claim
   checkable — so it stays at full --muted contrast rather than dropping to
   the hairline colour, which measured about 1.6:1 on this background and was
   effectively decorative. It is held back by face, size and letterspacing
   instead: quiet to skim past, legible to the one visitor who looks it up. */
.biblio-isbn{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.06em;
  color:var(--muted);font-variant-numeric:tabular-nums}
@media(min-width:900px){
  .biblio-grid{grid-template-columns:repeat(3,1fr);gap:clamp(2rem,4vw,3.4rem)}
  /* The middle label runs to two lines while the other two do not. Without a
     floor the three hairlines under them land at three different heights and
     the block reads as broken. Two lines of the label face is the floor. */
  .biblio-group{min-height:3.2em}
  .biblio-list{gap:1.15rem}
}

.dots,.timeline-dots{display:flex;flex-wrap:wrap;justify-content:center;gap:.55rem;
  margin-top:clamp(1.6rem,3vw,2.4rem)}
.dots[hidden],.timeline-dots[hidden]{display:none}
.dot-btn,.tl-dot-btn{appearance:none;-webkit-appearance:none;padding:0;background:none;border:0;cursor:pointer;
  width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent}
/* The tappable square is 22px; the mark inside it is 6px. Small dots must not
   mean small hit areas. */
.dot-btn::before,.tl-dot-btn::before{content:"";width:6px;height:6px;border-radius:50%;
  border:1px solid var(--ink);background:transparent;opacity:.35;
  transition:background .3s cubic-bezier(.22,.61,.36,1),opacity .3s,transform .3s cubic-bezier(.22,.61,.36,1)}
.dot-btn.is-active::before,.tl-dot-btn.is-active::before{background:var(--ink);opacity:1;transform:scale(1.15)}
.dot-btn:focus-visible,.tl-dot-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:2px}
@media (hover:hover){
  .dot-btn:hover::before,.tl-dot-btn:hover::before{opacity:1;border-color:var(--accent);background:var(--accent)}
}
@media (prefers-reduced-motion: reduce){ .dot-btn::before,.tl-dot-btn::before{transition:none} }
@media (prefers-reduced-motion: reduce){
  .dot-btn::before,.tl-dot-btn::before{transition:none}
  .tl-dot,.tl-place,.tl-item{transition:none}
}

/* Short desktop, continued. These must live AFTER the block above: they are
   the same specificity, so placed earlier they simply lose the cascade — which
   is exactly what happened on the first attempt. Each buys back a few pixels so
   the first card's year, title AND place clear the fold on a ~594px window. */
@media (min-width:1000px) and (max-height:900px){
  .timeline-bar{margin-top:.15rem;padding-bottom:.7rem}
  .timeline{padding-top:8px}
  .tl-item{padding-top:1.65rem}
  .tl-title{margin-bottom:.35rem}
}

/* --------------------------- Testimonials -------------------------- */
.voices{padding-block:var(--pad-block);background:var(--white);border-block:1px solid var(--line)}

/* One review at a time, in a native scroll-snap carousel. No slider library:
   the viewport is an overflow:auto strip, each slide is 100% wide, and snap
   does the rest. That means it still works with JS off, keyboard scrolling,
   and a trackpad flick — the JS only adds dots and autoplay on top.
   data-lenis-prevent lives on the viewport in the markup so smooth-scroll
   does not swallow the horizontal gesture (same fix as the timeline). */
/* overflow-y is `auto`, not hidden — hidden/clip stops Chromium accepting a
   horizontal wheel gesture, which is what made the carousel unscrollable with a
   trackpad. Safe here because --voice-h pins the box to the tallest slide, so
   nothing inside ever overflows vertically. Same rule as the timeline strip. */
.voice-viewport{overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;
  scrollbar-width:none}   /* no margin-top — see .section-head */
/* main.js drives the horizontal axis here (enableDragScroll with touch:true),
   so the browser must not also pan it. pan-y keeps the PAGE scrollable with a
   vertical swipe that starts inside the carousel. It is repeated on the
   children because touch-action is not inherited — it is read from the element
   actually touched, which is the <p> of the quote, not this box. */
.voice-viewport,.voice-viewport *{touch-action:pan-y}
.voice-viewport::-webkit-scrollbar{display:none}
.voice-viewport:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
/* Grab cursor on pointer devices only — see enableDragScroll() in main.js.
   A touch device gets native scrolling and no cursor, so this must not apply
   there or a stylus/hybrid machine shows a grab hand over a tap target. */
@media (pointer:fine){
  .voice-viewport{cursor:grab}
  .voice-viewport.is-dragging{cursor:grabbing;user-select:none;-webkit-user-select:none}
  .voice-viewport.is-dragging *{cursor:grabbing}
}
.voice-list{display:flex;gap:0;margin:0;padding:0;list-style:none}
/* flex-basis 100% + max-width 100% is what actually pins one slide per view;
   without the max-width the content can push a slide wider than the frame. */
/* NOTE: no `scroll-snap-stop:always`. It forbids passing a snap point, so a
   small wheel or drag increment gets dragged straight back to where it started
   and the carousel appears frozen — the exact "it will not scroll" bug. */
.voice{flex:0 0 100%;max-width:100%;scroll-snap-align:center;
  padding:clamp(.5rem,1.5vw,1rem) clamp(1rem,4vw,3rem);text-align:center;
  display:flex;flex-direction:column;align-items:center;justify-content:center}

/* ---- The aggregate line ------------------------------------------------
   Five stars printed five times down a band is decoration; five stars once,
   over a stated count, is evidence. The count is computed from the reviews
   themselves in index.php, so the two can never disagree. */
.voice-aggregate{display:flex;flex-wrap:wrap;align-items:center;justify-content:center;
  gap:.4rem .75rem;margin:0 0 clamp(1.8rem,3.5vw,2.6rem)}
.voice-aggregate .stars{gap:.34rem}
.voice-aggregate .star{width:14px;height:14px}
.voice-aggregate-text{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.16em;
  text-transform:uppercase;color:var(--muted)}

/* ---- Three across on a wide screen -------------------------------------
   One review at a time gave a 590px band four lines of text and made the
   section read as a slideshow. Three at once read as a body of opinion,
   which is the whole point of showing reviews at all. The carousel survives
   underneath: slides are a third of the frame instead of the whole of it,
   so scroll-snap, drag, keyboard and the dots all keep working unchanged. */

.voice-date{display:block;margin-top:.3rem;letter-spacing:.12em;color:var(--line)}
.voice-quote{margin:1.1rem 0 0}
/* Larger and in the display face — with one quote on screen it is the whole
   section, so it carries the weight the three-up grid used to share. */
.voice-quote p{margin:0;font-family:var(--display);font-weight:400;
  font-size:clamp(1.15rem,2.2vw,1.6rem);line-height:1.55;color:var(--ink);
  max-width:36ch;text-wrap:pretty}
.voice-name{margin:1.4rem 0 0;font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.2em;
  text-transform:uppercase;color:var(--muted)}
/* Equal-height slides so the dots do not jump as quotes of different lengths
   scroll past. Set from JS to the tallest slide; falls back to auto. */
.voice-viewport{min-height:var(--voice-h,auto)}

/* Stars. Drawn in CSS — no icon font, no SVG sprite, and a half star is just a
   gradient stop, so 3.5 renders honestly instead of being rounded up. */
/* Slimmer star than the default five-pointer (inner radius 0.4 of the outer,
   not 0.5) at 12px with a wider gap: the chunky solid star is a review-site
   convention, and five of them at 13px were the loudest object in a quiet
   testimonial. Longer points read as engraved rather than stamped. */
.stars{display:inline-flex;gap:.3rem;line-height:1}
.star{width:12px;height:12px;flex:0 0 auto;background:var(--line);
  -webkit-mask:var(--star-mask) center/contain no-repeat;mask:var(--star-mask) center/contain no-repeat}
/* Ink, not the brand red. Five red stars are the most e-commerce thing that
   could appear on a page selling six-figure paintings — they read as a product
   rating widget. In ink they read as a mark in a catalogue. */
.star.is-full{background:var(--ink)}
.star.is-half{background:linear-gradient(to right,var(--ink) 50%,var(--line) 50%)}
:root{--star-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 1l2.59 7.44L22.46 8.6l-6.28 4.76 2.29 7.54L12 16.4l-6.47 4.5 2.29-7.54L1.54 8.6l7.87-.16z'/%3E%3C/svg%3E")}

/* ----------------------------- FAQ --------------------------------- */
.faq{padding-block:var(--pad-block)}
/* Was capped at 820px for a single centred column. Now the standard wrap, so
   the FAQ can sit left with a second column beside it. */
.faq-wrap{margin-inline:auto}
/* No margin-top: the gap under the heading is now .section-head's job, the
   same as every other section. This used to carry its own clamp(2.8rem,5vw,
   4.4rem) to make up for a head margin that was too small everywhere, which
   left the FAQ heading sitting 46px clear while the others sat at 17px. */
.faq-grid{display:grid;grid-template-columns:1fr;gap:clamp(2.4rem,5vw,3.5rem)}
.faq-main{min-width:0}
@media(min-width:1000px){
  /* The questions get the greater share; the aside is a companion, not a peer. */
  .faq-grid{grid-template-columns:minmax(0,1.45fr) minmax(0,1fr);
    gap:clamp(3rem,5vw,5rem);align-items:start}
  .faq-aside{position:sticky;top:calc(var(--header-h,66px) + 30px)}
}

/* The companion image. Framed the way the gallery frames a work — a hairline,
   a mount of white around the image, and the credit in small caps below — so
   it reads as a piece hung beside the questions rather than as decoration. */
.faq-figure{margin:0;background:var(--white);border:1px solid var(--line);
  padding:clamp(.9rem,2vw,1.4rem)}
.faq-figure img{display:block;width:100%;height:auto}
.faq-figure figcaption{display:flex;flex-direction:column;gap:.2rem;
  margin-top:clamp(.9rem,2vw,1.2rem);padding-top:.85rem;border-top:1px solid var(--line);
  font-family:var(--label);font-size:.62rem;letter-spacing:.18em;text-transform:uppercase}
.faq-figure-artist{color:var(--ink)}
.faq-figure-title{font-family:var(--display);font-size:1rem;letter-spacing:0;
  text-transform:none;color:var(--ink)}
.faq-figure-year{color:var(--muted)}
/* No top hairline. It was there to close the list into a bounded block, but
   with the heading now sitting well clear above it the rule read as a stray
   line under the title rather than as the top of a list. Each question keeps
   its own bottom border, so the list still reads as a set of rows. */
.faq-list{border-top:0}
.faq-item{border-bottom:1px solid var(--line)}
.faq-item summary{list-style:none;cursor:pointer;display:flex;justify-content:space-between;
  align-items:baseline;gap:1.5rem;padding:1.4rem 0;
  font-family:var(--display);font-size:clamp(1.08rem,1.9vw,1.3rem);font-weight:400;
  letter-spacing:-.005em;line-height:1.35;color:var(--ink);
  transition:color .3s cubic-bezier(.22,.61,.36,1);
  -webkit-tap-highlight-color:transparent}
.faq-item summary::-webkit-details-marker{display:none}
.faq-item summary::marker{content:""}
@media (hover:hover){ .faq-item summary:hover{color:var(--accent)} }
.faq-item summary:focus-visible{outline:2px solid var(--accent);outline-offset:4px}
.faq-q{flex:1 1 auto;text-wrap:pretty}

/* Hairline plus that turns a quarter and loses its upright to become a minus. */
.faq-icon{position:relative;flex:0 0 15px;width:15px;height:15px;align-self:center;
  transition:transform .42s cubic-bezier(.22,.61,.36,1)}
.faq-icon::before,.faq-icon::after{content:"";position:absolute;background:currentColor;
  transition:transform .42s cubic-bezier(.22,.61,.36,1)}
.faq-icon::before{top:7px;left:0;width:15px;height:1px}
.faq-icon::after{top:0;left:7px;width:1px;height:15px}
.faq-item[open] .faq-icon{transform:rotate(180deg)}
.faq-item[open] .faq-icon::after{transform:scaleY(0)}

/* Height is animated by main.js; padding lives on the <p> so a closed panel
   really is zero-height. */
.faq-answer{overflow:hidden;transition:height .48s cubic-bezier(.22,.61,.36,1)}
/* The top padding is what separates the answer from the accent rule that
   draws itself under an open question. With none, the first line sat directly
   on the hairline and the whole panel read as cramped. It lives on the <p>,
   not on .faq-answer: that box is height-animated, and padding on a
   height-animated box never collapses to nothing on close.
   panel.scrollHeight in main.js includes this, so the animation still measures
   correctly — change the value freely, but keep it here. */
.faq-answer p{margin:0;padding:1.15rem 0 1.9rem;color:var(--muted);max-width:64ch;line-height:1.75;
  /* The box opening was the only thing that moved; the text was simply there
     the instant it had room, which made a 480ms animation look like a jump.
     It rises into place instead, a beat behind the box, 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
     before it. */
  opacity:0;transform:translateY(-8px);
  transition:opacity .34s ease,transform .44s cubic-bezier(.22,.61,.36,1)}
.faq-item[open] .faq-answer p{opacity:1;transform:none;transition-delay:.1s}
/* The open question steps forward: full ink instead of the resting weight, and
   a hairline that draws itself in from the left under the row. */
.faq-item summary{position:relative}
/* No background — the accent rule under an open question is off. The element
   is kept (with its transform and transition) so re-enabling it is one
   `background:` declaration rather than rebuilding the rule; it paints nothing
   as it stands. */
.faq-item summary::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:1px;
  transform:scaleX(0);transform-origin:left;
  transition:transform .5s cubic-bezier(.22,.61,.36,1)}
.faq-item[open] summary::after{transform:scaleX(1)}
@media (prefers-reduced-motion: reduce){
  .faq-answer,.faq-answer p,.faq-icon,.faq-icon::before,.faq-icon::after,
  .faq-item summary::after{transition:none}
}

/* Anyone still reading at the bottom of the FAQ has a question we did not
   answer — give them somewhere to go rather than a dead end. */
.faq-more{margin:2.4rem 0 0;text-align:center;color:var(--muted);
  font-family:var(--label);font-size:var(--fs-label-sm);letter-spacing:.14em;text-transform:uppercase}
.faq-more a{color:var(--ink);border-bottom:1px solid var(--line);padding-bottom:2px;
  transition:border-color .3s}
@media (hover:hover){ .faq-more a:hover{border-color:var(--ink)} }

/* --------------------------- Enquire ------------------------------- */
.enquire{padding-block:var(--pad-block);background:var(--panel)}
.enquire-grid{display:flex;flex-direction:column;gap:2.5rem}
/* Every .section-head measured 124px above / 26px below and clamp(...,2.9rem).
   This one alone ran at 2.6rem with a .5em gap — it is a column heading, not a
   centred section head, so it keeps its left alignment but now shares the same
   scale and the same rhythm underneath. */
.enquire-intro h2{font-size:clamp(1.95rem,3.9vw,2.9rem);font-weight:400;
  letter-spacing:-.012em;line-height:1.12;margin-bottom:1.25em}
.enquire-intro>p{color:var(--muted);max-width:44ch}
/* Short-desktop override lives HERE, not up with the .section-head one: the
   base rule above is the same specificity and sits later in the file, so an
   earlier media query loses the cascade and the two headings drift apart
   (52px against 42px at 1366x768). Same trap as the timeline overrides. */
@media (min-width:1000px) and (max-height:900px){
  .enquire-intro h2{font-size:clamp(1.8rem,3.2vw,2.35rem);margin-bottom:1em}
}
/* ===================== PRIVATE ADVISORY =============================
   This column used to repeat the address, phone, email and the full
   opening-hours table — every one of which is already in the footer. Four
   things to read, beside a form, and nothing to do. It is now a short case for
   getting in touch and one action: call someone.

   The three points are drawn from answers already given in $faqs, so the page
   cannot contradict itself. Set as a labelled spec list, the same vocabulary
   as the lightbox .spec rows — it reads as a catalogue entry for the service
   rather than as marketing bullets. */
.advisory-lead{color:var(--muted);max-width:44ch}

.advisory-points{list-style:none;margin:clamp(1.8rem,3.5vw,2.6rem) 0 0;padding:0;
  border-top:1px solid var(--line);max-width:44ch}
.advisory-points li{display:grid;gap:.4rem;padding:1.05rem 0;border-bottom:1px solid var(--line)}
.advisory-point-label{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.22em;
  text-transform:uppercase;color:var(--muted)}
.advisory-point-text{color:var(--ink);font-size:.98rem;line-height:1.6;text-wrap:pretty}

/* The call. Given the weight of a headline — the number IS the call to action,
   so it is set in the display face at heading scale rather than hidden in a
   list of contact details. */
.advisory-call{margin-top:clamp(1.8rem,3.5vw,2.6rem)}
.advisory-call-label{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.22em;
  text-transform:uppercase;color:var(--muted);margin:0 0 .55rem}
.advisory-tel{display:inline-block;font-family:var(--display);font-weight:400;
  font-size:clamp(1.5rem,2.6vw,2rem);letter-spacing:-.01em;line-height:1.1;color:var(--ink);
  border-bottom:1px solid var(--line);padding-bottom:.12em;
  transition:border-color .35s cubic-bezier(.22,.61,.36,1),color .35s cubic-bezier(.22,.61,.36,1)}
@media (hover:hover){ .advisory-tel:hover{border-color:var(--ink);color:var(--accent)} }
.advisory-tel:focus-visible{outline:2px solid var(--accent);outline-offset:4px}

/* Reuses the .cta-status dot + small caps from the sticky bar, left-aligned
   here. Two classes so it wins over .cta-status regardless of source order —
   the same cascade trap noted on .menu-state. */
.enquire-intro .advisory-state{justify-content:flex-start;margin:.85rem 0 0;
  font-size:var(--fs-label-xs)}
.advisory-call-note{color:var(--muted);font-size:.92rem;line-height:1.6;margin:.9rem 0 0;max-width:38ch}
.advisory-note{font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.22em;
  text-transform:uppercase;color:var(--muted);margin:clamp(1.6rem,3vw,2.2rem) 0 0}

.contact-note{font-family:var(--label);font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}
.price-note{font-size:.9rem;color:var(--muted);font-style:italic;max-width:46ch}

.enquire-form-wrap{background:var(--white);border:1px solid var(--line);padding:clamp(1.4rem,4vw,2.4rem)}
/* The form follows you down the advisory column — the same rule as
   .faq-aside: sticky from 1000px, offset by the live header height, no
   viewport-height condition. Matching that behaviour was the explicit brief.
   `align-self:flex-start` is what makes sticky possible at all: a stretched
   flex item is already as tall as the row and has nowhere to travel. (The
   FAQ aside needs no equivalent because .faq-grid is a grid with
   align-items:start.)
   Trade-off to know about: the form is ~700px tall, so on a viewport under
   about 820px it is taller than the space beneath the header and the bottom
   of it — the Send button — sits below the fold while pinned. That is the
   same trade the FAQ figure makes. */
@media (min-width:1000px){
  .enquire-form-wrap{position:sticky;top:calc(var(--header-h,66px) + 30px);align-self:flex-start}
}
.form-title{font-family:var(--display);font-size:1.5rem;font-weight:600;margin:0 0 1.4rem}
.form-grid{display:grid;grid-template-columns:1fr;column-gap:1rem}
@media(min-width:560px){.form-grid{grid-template-columns:1fr 1fr}}
.field{margin-bottom:1.1rem}
.field label{display:block;font-family:var(--label);font-size:.72rem;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);margin-bottom:.4rem}
.field .req{color:var(--accent)}
.field .opt{text-transform:none;letter-spacing:0;font-size:.68rem}
.field input,.field select,.field textarea{
  width:100%;font-family:var(--body);font-size:1rem;color:var(--ink);
  /* --white, not --bg. On the white form panel the page's off-white read as a
     FILLED GREY BOX — the only filled rectangles on a page built entirely from
     hairlines on paper, and the thing that made this section look like a web
     form rather than part of the gallery. The field is now defined by its
     hairline, like every other object here; focus darkens that line to ink. */
  background:var(--white);border:1px solid var(--line);padding:.8rem .9rem;border-radius:0;
  transition:border-color .15s,box-shadow .15s;
}
.field input:focus,.field select:focus,.field textarea:focus{outline:none;border-color:var(--ink);box-shadow:0 0 0 3px rgba(28,26,23,.08)}
.field textarea{resize:none;min-height:6rem;overflow-y:hidden;line-height:1.5}
.consent .checkbox{display:flex;gap:.6rem;align-items:flex-start;text-transform:none;letter-spacing:0;font-family:var(--body);font-size:.86rem;color:var(--muted)}
/* The consent box was the browser's own control — a rounded blue system
   checkbox, the one object on the page drawn by an operating system rather
   than by us, sitting directly under a six-figure enquiry form. It is now the
   same hairline square as everything else, with a drawn check (1px, square
   caps) that appears when it is ticked. `appearance:none` is what lets us
   draw it; the input is still a real checkbox, so keyboard, form validation
   and the required attribute are untouched. */
.consent .checkbox input{appearance:none;-webkit-appearance:none;
  width:16px;height:16px;flex:0 0 auto;margin:.2rem 0 0;padding:0;
  background:var(--white);border:1px solid var(--line);border-radius:2px;
  cursor:pointer;transition:border-color .3s cubic-bezier(.22,.61,.36,1),background .3s cubic-bezier(.22,.61,.36,1)}
.consent .checkbox input:checked{border-color:var(--ink);
  background:var(--white) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.2 8.4l3.1 3.1 6.5-6.9' fill='none' stroke='%231c1a17' stroke-width='1.1' stroke-linecap='square'/%3E%3C/svg%3E") center/12px no-repeat}
@media (hover:hover){ .consent .checkbox input:hover{border-color:var(--muted)} }
.consent .checkbox input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* .field input:focus paints a 3px glow — right for a 48px text field, a halo
   round a 16px box. The checkbox has its own focus-visible outline above. */
.consent .checkbox input:focus{box-shadow:none;border-color:var(--ink)}
.consent .checkbox a{border-bottom:1px solid var(--muted)}
.form-fineprint{font-size:.78rem;color:var(--muted);text-align:center;margin:.9rem 0 0}
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.form-success{background:var(--white);border:1px solid var(--ink);padding:1.6rem}
.form-success h3{font-family:var(--display);font-size:1.4rem;margin-bottom:.5rem}
.form-error{background:#fdeceb;border:1px solid var(--accent);color:#8a231b;padding:.7rem .9rem;margin-bottom:1rem;font-size:.88rem}

@media(min-width:1000px){
  .enquire-grid{flex-direction:row;align-items:flex-start;gap:4rem}
  /* NOT sticky any more. The advisory column is the TALLER of the two now
     (measured 908-976px against the form's 673-695px), so pinning it did
     nothing — it was already the column that governs the section's height. */
  .enquire-intro{flex:0 0 42%}
  .enquire-form-wrap{flex:1}
}

/* ---------------------------- Footer ------------------------------- */
/* Centred logo with links flanking left & right; centred copyright below. */
/* ------------------------------ Footer ------------------------------
   A four-column masthead footer: identity, two link columns, contact.
   The old version was two links, a logo and a copyright line — fine for a
   brochure page, thin for a gallery asking for six-figure enquiries. All
   contact detail comes from $cfg, so it can never drift from the rest of
   the page. */
/* --bg, not --panel: the enquiry band directly above is already cream, so a
   cream footer merged the two into one undifferentiated block. */
.site-footer{background:var(--bg);border-top:1px solid var(--line);
  padding-block:clamp(52px,7vw,84px) clamp(24px,3vw,32px)}
.footer-grid{display:grid;grid-template-columns:1fr;gap:clamp(2.2rem,4vw,3rem)}

/* block, not inline-block: an inline-block logo carries the line-box's
   descender space under it, which pushed the paragraph ~20px below where the
   link columns start and broke the row. */
.footer-logo{display:block;width:max-content;max-width:100%}
.footer-logo img{height:clamp(38px,5vw,48px);width:auto;display:block}
/* Sits on the same line as the first link of the columns beside it, so the
   four columns read as one row rather than as a logo with three lists. */
.footer-blurb{margin:1.35rem 0 0;max-width:34ch;color:var(--muted);
  font-size:.95rem;line-height:1.7;text-wrap:pretty}

/* A short accent rule under each column head. One 22px hairline is enough to
   anchor a column of links and to say the page was designed rather than
   assembled — the same gesture the FAQ uses under an open question. */
.footer-head{position:relative;font-family:var(--label);font-size:var(--fs-label-xs);
  letter-spacing:.26em;text-transform:uppercase;color:var(--muted);
  margin:0 0 1.05rem;padding-bottom:.8rem}
.footer-head::after{content:"";position:absolute;left:0;bottom:0;width:22px;height:1px;
  background:var(--accent)}
.footer-col{display:flex;flex-direction:column;align-items:flex-start;gap:.62rem}
/* Sentence case, not caps. A column of tracked capitals is a shouting match;
   caps are reserved for the headings so the hierarchy does the work. */
.footer-col a{font-family:var(--label);font-size:var(--fs-label-md);letter-spacing:.02em;
  color:var(--ink);line-height:1.55;
  border-bottom:1px solid transparent;padding-bottom:1px;
  transition:border-color .3s cubic-bezier(.22,.61,.36,1),color .3s}
@media (hover:hover){ .footer-col a:hover{border-color:var(--ink)} }
.footer-col a:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
/* The phone and email are actions and carry the same ink as every other link
   in the footer. Only the address — which is a fact, not a thing to press —
   stays quiet. They were all muted before, which made the one column a visitor
   is most likely to use the faintest thing on the page. */
.footer-contact a{color:var(--ink)}
.footer-contact a:first-of-type{color:var(--muted);line-height:1.6}
@media (hover:hover){ .footer-contact a:hover{color:var(--ink);border-color:var(--ink)} }

/* Hours as a spec list: day left, time right, on hairlines. As loose spans the
   two ran together and nothing lined up down the column. */
.footer-hours{margin:1.35rem 0 0;max-width:270px}
.footer-hours-row{display:flex;align-items:baseline;justify-content:space-between;
  gap:1rem;padding:.42rem 0;border-top:1px solid var(--line)}
.footer-hours-row:first-child{border-top:0;padding-top:0}
.footer-hours dt{font-family:var(--label);font-size:.68rem;letter-spacing:.06em;color:var(--muted)}
.footer-hours dd{margin:0;font-family:var(--label);font-size:.68rem;letter-spacing:.06em;
  color:var(--ink);text-align:right;white-space:nowrap}
.footer-hours dd.is-closed{color:var(--muted);font-style:italic}

/* ------------------------------ Social ------------------------------
   One component, two placements. Hairline squares rather than filled circles:
   a filled social badge is a consumer-app convention and would be the loudest
   thing in a footer built entirely from hairlines. The mark is 16px inside a
   38px box, so the touch target clears the 44px guidance once the row's own
   gap is counted, and the glyph itself stays small enough to read as a mark
   rather than as a button.
   `currentColor` on the SVG means the icon inherits the link's colour and its
   hover transition without a second rule. */
.social-row{display:flex;list-style:none;margin:0;padding:0;gap:.55rem}
.social-row a{display:flex;align-items:center;justify-content:center;
  width:38px;height:38px;padding:0;color:var(--muted);
  border:1px solid var(--line);border-radius:2px;
  transition:color .3s cubic-bezier(.22,.61,.36,1),border-color .3s cubic-bezier(.22,.61,.36,1),
             background .3s cubic-bezier(.22,.61,.36,1)}
.social-row svg{display:block}
@media (hover:hover){
  .social-row a:hover{color:var(--white);background:var(--ink);border-color:var(--ink)}
}
.social-row a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.social-row--footer{margin-top:clamp(1.6rem,3vw,2.1rem)}
/* In the menu the row sits under the phone and email, so it needs less air
   above it and a little below to clear the panel's own padding. */
.social-row--menu{margin:1.1rem 0 .2rem}

/* Credentials — the two facts a six-figure buyer checks, restated at the end. */
/* Set between two rules that run to the edges of the wrap, rather than as a
   line of small print under a full-width border. It reads as a seal — which is
   what the sentence is — and it takes one of the three horizontal rules out of
   a footer that had too many.
   NOTE: this treatment assumes ONE line of text. Add a second credential and
   the flanking rules will wrap away from it; go back to a plain bordered row
   if that ever happens. */
.footer-cred{display:flex;align-items:center;gap:clamp(1rem,3vw,2.2rem);
  margin-top:clamp(2.8rem,5.5vw,4.2rem);margin-bottom:clamp(2.4rem,4.5vw,3.4rem)}
.footer-cred::before,.footer-cred::after{content:"";flex:1 1 auto;height:1px;
  background:var(--line)}
/* flex:0 1 auto, NOT 0 0 auto. At 0 0 the span could not shrink, and on a
   390px screen this 40-character tracked line ran 21px past the viewport and
   gave the whole page a horizontal scrollbar. */
.footer-cred span{flex:0 1 auto;min-width:0;font-family:var(--label);font-size:.6rem;
  letter-spacing:.24em;text-transform:uppercase;color:var(--muted);text-align:center;
  text-wrap:balance}

/* Below 640px there is no room for a line of tracked capitals AND two rules
   beside it. The rules go and it becomes a plain centred credential under a
   border — the treatment it had before, which is the right one at this width. */
@media(max-width:639px){
  .footer-cred{display:block;text-align:center;
    padding-top:clamp(1.6rem,5vw,2.2rem);border-top:1px solid var(--line);
    margin-bottom:clamp(1.8rem,5vw,2.6rem)}
  .footer-cred::before,.footer-cred::after{display:none}
}
/* No separator rule between the two. An absolutely-positioned divider is only
   correct while they sit on ONE line; the moment they wrap it strands itself
   against the left edge of the page, and the width at which they wrap moves
   with the copy. The gap alone reads fine and cannot break. */

/* The flanking rules of .footer-cred above are the divider now, so this row
   needs no border of its own — three stacked hairlines in the space of 100px
   was the clutter that made the end of the footer feel busy. */
.footer-legal{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;
  gap:.5rem 1.6rem;margin-top:0}
.footer-legal p{margin:0;font-family:var(--label);font-size:.64rem;letter-spacing:.1em;color:var(--muted)}
.footer-legal-nav{display:flex;gap:1.4rem}
.footer-legal-nav a{font-family:var(--label);font-size:.64rem;letter-spacing:.1em;color:var(--muted);
  border-bottom:1px solid transparent;transition:border-color .3s,color .3s}
@media (hover:hover){ .footer-legal-nav a:hover{color:var(--ink);border-color:var(--line)} }
/* The arrow lifts on hover — the only piece of motion in the footer, and it
   points at what the link does. */
.footer-top-link span{display:inline-flex;align-items:center;margin-left:.4rem;
  position:relative;top:1px;
  transition:transform .35s cubic-bezier(.22,.61,.36,1)}
@media (hover:hover){ .footer-top-link:hover span{transform:translateY(-3px)} }
@media (prefers-reduced-motion: reduce){ .footer-top-link span{transition:none} }

@media(min-width:640px){
  .footer-grid{grid-template-columns:1fr 1fr;gap:2.6rem clamp(2rem,4vw,3rem)}
  .footer-brand{grid-column:1 / -1}
}
@media(min-width:1000px){
  /* Identity gets the widest column; the three link columns are even. */
  .footer-grid{grid-template-columns:1.5fr 1fr 1fr 1.25fr;gap:clamp(2rem,4vw,3.5rem)}
  .footer-brand{grid-column:auto}
  .footer-legal{justify-content:space-between}
}

/* ------------------------ Sticky mobile CTA ------------------------ */
/* HIDDEN until the visitor has been past the hero. Landing on a six-figure
   artwork page and being handed "Call / Enquire" before you have seen a single
   work is the digital equivalent of being approached at the door. The bar
   arrives once someone has actually started looking, and slides away again if
   they return to the top. main.js adds .is-revealed — see the hero observer.

   translateY(110%) rather than 100% so the box-shadow clears the screen edge
   too. `visibility` and `pointer-events` are what make it genuinely gone: a
   bar sitting off-screen at opacity 0 is still tappable and still read out by
   a screen reader. visibility is transitioned with a delay on the way out so
   it only flips after the slide finishes. */
/* ONE ROW, EDGE TO EDGE — built to the client's reference.
   Status line left, a single gold action right, dismiss last. It went through
   an inset frosted card first; the card floated, but a two-row card with a
   Call/Enquire pair was carrying four decisions in 100px of screen. This is
   the same information in 66px: where the gallery is (open or closed), and one
   thing to press.

   Full-bleed with a top hairline rather than a floating object, so it reads as
   the page's own foot rather than as something laid over it. Opaque --white,
   not a frosted panel: the artwork scrolling underneath a translucent bar was
   the busiest thing on the screen.

   Geometry matched to the reference at 390px: 66px tall, ~13px of padding
   above and below, the mark 20px in from the left edge. */
.sticky-cta{position:fixed;z-index:90;left:0;right:0;bottom:0;
  display:flex;align-items:center;gap:.55rem;
  padding:.62rem clamp(12px,4vw,18px);
  padding-bottom:calc(.62rem + env(safe-area-inset-bottom));
  background:var(--white);border-top:1px solid var(--line);
  /* Just enough to lift it off the page. A slab shadow here would put back the
     "app furniture" the card was avoiding. */
  box-shadow:0 -10px 26px -22px rgba(28,26,23,.5);
  transform:translateY(calc(100% + 24px));opacity:0;visibility:hidden;pointer-events:none;
  transition:transform .5s cubic-bezier(.22,.61,.36,1),opacity .35s ease,
             visibility 0s linear .5s}
.sticky-cta.is-revealed{transform:none;opacity:1;visibility:visible;pointer-events:auto;
  transition:transform .5s cubic-bezier(.22,.61,.36,1),opacity .35s ease,
             visibility 0s linear 0s}
@media (prefers-reduced-motion: reduce){
  .sticky-cta{transition:none}
}
/* The button sizes to its own label and stays put; the status line is what
   takes the slack. A full-width button in a one-row bar reads as a banner. */
.sticky-cta .btn{min-width:0;flex:0 0 auto}
/* Sized to the reference: ~96px wide, 44px tall — the tap-target floor, and
   no wider than its own label, so the status line keeps the room it needs to
   say the whole sentence. */
.sticky-cta .cta-enquire{padding:.72rem 1rem;font-size:var(--fs-label-sm);letter-spacing:.13em}

.cta-enquire{white-space:nowrap}
/* --- While the enquiry form is on screen -------------------------------
   With one action in the bar and that action being "Enquire", there is nothing
   left to offer someone who is already at the form — so the whole bar leaves,
   by the same journey it uses for dismiss and duck (below), and comes back
   when the form scrolls away. Previously it swapped in a Call button here;
   that button is gone, and the number is a tap away in the menu. */
/* Open / closed line. Small, but the dot carries the state at a glance. */
/* Inside the card this is a caption, not a banner — one step quieter than it
   was, and it sits above the buttons as the reason to press one.
   The inline padding keeps the centred text clear of the dismiss control in
   the corner, including the long "Closed - opens Tuesday 9.30am" wording. */
/* SENTENCE CASE, not small caps — the reference's own treatment, and the right
   one: this is a sentence a person reads ("Closed · opens Tuesday 9.30am"),
   not a label. Uppercase at .54rem made a piece of live information look like
   a category heading, and the tracking cost it the width it needed to fit on
   one line beside a button. It shrinks and ellipses if it must; the button and
   the dismiss never do. */
.cta-status{display:flex;align-items:center;gap:.45rem;margin:0;flex:1 1 auto;min-width:0;
  font-family:var(--label);font-size:.72rem;letter-spacing:.01em;text-transform:none;
  line-height:1.3;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

/* Dismiss. Deliberately quiet — it is an escape hatch, not an action — but with
   a 40px hit area, because a control too small to hit is worse than none. */
/* Still a 34px hit area, but pulled tight into the row with negative margins
   so it costs the bar no height of its own. */
.cta-dismiss{flex:0 0 auto;width:34px;height:34px;margin:-9px -10px -9px -6px;
  display:flex;align-items:center;justify-content:center;
  appearance:none;-webkit-appearance:none;background:none;border:0;padding:0;
  color:var(--muted);cursor:pointer;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent;
  transition:color .3s ease}
@media (hover:hover){ .cta-dismiss:hover{color:var(--ink)} }
.cta-dismiss:focus-visible{outline:2px solid var(--accent);outline-offset:-4px;border-radius:3px}

/* Closed for the session, or ducked while the visitor is typing in the form.
   Same journey off-screen as the un-revealed state, so there is one animation
   to reason about rather than three. */
.sticky-cta.is-dismissed,
.sticky-cta.at-enquire,
.sticky-cta.is-ducked{transform:translateY(calc(100% + 24px));opacity:0;
  visibility:hidden;pointer-events:none;
  transition:transform .38s cubic-bezier(.22,.61,.36,1),opacity .25s ease,
             visibility 0s linear .38s}
.cta-status[hidden]{display:none}
.cta-status::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--muted);flex:0 0 auto}
/* Both states carry colour. Closed is Andipa red — the client's call, and
   defensible: this is the gallery's own accent, not a browser error red, and
   at 5px beside the word "Closed" it reads as a state light rather than as a
   warning. (An earlier pass had it muted ink to match a reference; reverted.)
   Green for open, so the two are told apart at a glance and not only by the
   sentence beside them. The dot is drawn by `.cta-status::before` — there is
   no separate .status-dot element; every place that shows the state (sticky
   bar, menu foot, Private Advisory) uses this one class. */
.cta-status.is-open::before{background:#2f7d4f}
.cta-status.is-closed::before{background:var(--accent)}
/* The one filled button on the site. Gold at rest; the sweep is the SAME gold
   darkened rather than a contrasting colour, so pressing it deepens the button
   instead of inverting it — an inversion to white made it flash on a phone,
   where "hover" is really the moment of the tap. */
.btn-solid{background:var(--gold);color:var(--white);border-color:var(--gold)}
.btn-solid::before{background:var(--gold-deep)}
.btn-solid:focus-visible{color:var(--white);border-color:var(--gold-deep)}
@media (hover:hover){ .btn-solid:hover{color:var(--white);border-color:var(--gold-deep)} }
/* Narrow phones. "Closed · opens Wednesday 10.30am" is the longest sentence
   this line ever has to carry and it needs 203px at the default size; a 360px
   Android leaves 185px beside the button. One step down on the type and a
   little off the button's flanks buys the 20px rather than truncating live
   information. */
@media(max-width:400px){
  .cta-status{font-size:.67rem}
  .sticky-cta .cta-enquire{padding:.72rem .85rem}
}
/* Below ~340px there is no size that fits it on one line without shrinking the
   type past reading. It wraps instead — the bar grows by a line, which is the
   right trade for a sentence that stays whole. */
@media(max-width:339px){
  .cta-status{white-space:normal;line-height:1.25}
}
@media(min-width:1000px){
  .sticky-cta{display:none}
  body{padding-bottom:0}
}

/* ======================= FULL-SCREEN STAGE ==========================
   The viewing room. Deliberately NOT styled like the rest of the site: a
   near-black ground so the eye has nothing to compare the work against, and
   chrome reduced to two slim bars. The artwork is fitted BETWEEN those bars
   rather than under them, so the rule that nothing is ever laid over the work
   survives into full screen. */
.lb-stage{position:fixed;inset:0;z-index:210;display:flex;flex-direction:column;
  background:#111013;opacity:0;transition:opacity .3s cubic-bezier(.22,.61,.36,1)}
.lb-stage[hidden]{display:none}
.lb-stage.is-open{opacity:1}

.lb-stage-top,.lb-stage-foot{flex:0 0 auto;display:flex;align-items:center;gap:1rem;
  padding:.7rem clamp(12px,3vw,24px);color:rgba(255,255,255,.7);
  padding-top:calc(.7rem + env(safe-area-inset-top))}
.lb-stage-foot{justify-content:center;gap:1.2rem;
  padding-top:.7rem;padding-bottom:calc(.7rem + env(safe-area-inset-bottom))}
.lb-stage-top{justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.09)}
.lb-stage-foot{border-top:1px solid rgba(255,255,255,.09)}

.lb-stage-label{margin:0;font-family:var(--display);font-size:1.05rem;color:rgba(255,255,255,.92);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lb-stage-tools{display:flex;align-items:center;gap:.4rem;flex:0 0 auto}
.lb-stage-level{min-width:4ch;text-align:center;font-family:var(--label);
  font-size:var(--fs-label-xs);letter-spacing:.1em;color:rgba(255,255,255,.6)}
.lb-stage-count{margin:0;display:flex;flex-direction:column;align-items:center;gap:.2rem;
  font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.2em;text-transform:uppercase;
  color:rgba(255,255,255,.55);text-align:center}
.lb-stage-hint{color:rgba(255,255,255,.32);letter-spacing:.16em}

/* Controls sit on the dark ground, never on the artwork. */
.lb-tool{width:40px;height:40px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
  color:rgba(255,255,255,.8);background:transparent;border:1px solid rgba(255,255,255,.22);
  border-radius:3px;cursor:pointer;touch-action:manipulation;
  transition:background .25s,color .25s,border-color .25s}
@media (hover:hover){ .lb-tool:hover{background:rgba(255,255,255,.95);color:#111013;border-color:transparent} }
.lb-tool:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
/* On the dark ground a 1px white hairline reads lighter than the same line
   on paper, so the close mark gets a touch more presence than its neighbours
   rather than a bigger glyph. */
.lb-tool-close{color:rgba(255,255,255,.92)}

/* The frame clips; the <img> inside is what moves. touch-action:none because
   every gesture in here — pan, pinch, swipe, double-tap — is ours, and the
   browser panning the page underneath would fight all four. It is repeated on
   the image because touch-action is read from the element actually touched. */
.lb-stage-frame{flex:1 1 auto;min-height:0;position:relative;overflow:hidden;
  display:flex;align-items:center;justify-content:center;
  touch-action:none;cursor:zoom-in;-webkit-user-select:none;user-select:none}
.lb-stage.is-zoomed .lb-stage-frame{cursor:grab}
.lb-stage.is-zoomed.is-panning .lb-stage-frame{cursor:grabbing}
.lb-stage-frame img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;
  touch-action:none;-webkit-user-drag:none;user-select:none;-webkit-user-select:none;
  transform-origin:center center;will-change:transform;
  transition:transform .28s cubic-bezier(.22,.61,.36,1)}
/* No transition while a finger is down, or the artwork lags behind the hand. */
.lb-stage.is-panning .lb-stage-frame img{transition:none}
@media (prefers-reduced-motion: reduce){
  .lb-stage,.lb-stage-frame img{transition:none}
}
@media(max-width:600px){
  .lb-stage-label{font-size:.92rem}
  .lb-tool{width:36px;height:36px}
}

/* --------------------- Reduced motion ------------------------------ */
@media(prefers-reduced-motion:reduce){
  *{animation-duration:.001ms!important;transition-duration:.001ms!important}
  html{scroll-behavior:auto}
}
/* Anchor jumps land flush on the section's colour line: the offset equals the
   live sticky-header height (--header-h, set from JS; falls back to 66px). No
   gap, so each section's background meets the header's bottom border exactly.
   scroll-behavior is the no-JS fallback — main.js runs a custom eased scroll. */
html{scroll-behavior:smooth;scroll-padding-top:var(--header-h,66px)}

/* ----------------------------- Lightbox ---------------------------- */
.lightbox[hidden]{display:none}
.lightbox{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;padding:clamp(14px,4vw,48px);
  opacity:0;transition:opacity .5s cubic-bezier(.22,.61,.36,1)}
.lightbox.is-open{opacity:1}
.lightbox-overlay{position:absolute;inset:0;background:rgba(20,18,16,.86);backdrop-filter:blur(3px)}
/* Slow, composed entrance — the dialog settles up rather than snapping in. */
.lightbox-dialog{position:relative;z-index:1;display:flex;flex-direction:column;max-width:1180px;width:100%;max-height:96vh;max-height:96svh;background:var(--white);border:1px solid var(--line);border-radius:4px;overflow:hidden;
  box-shadow:0 40px 90px -40px rgba(0,0,0,.55);
  opacity:0;transform:translateY(18px) scale(.985);
  transition:opacity .55s cubic-bezier(.22,.61,.36,1),transform .65s cubic-bezier(.22,.61,.36,1)}
.lightbox.is-open .lightbox-dialog{opacity:1;transform:none}
/* Content crossfade when stepping between works (no open/close needed). */
/* Stepping between works. The artwork leaves in the direction of travel and the
   next one arrives from the opposite side, so forwards and backwards feel
   different — a symmetric crossfade gave no sense of direction. The info panel
   only fades, so the text does not slide about under the image.
   --lb-out / --lb-in are set per step by main.js. */
.lightbox-media{transition:opacity .32s cubic-bezier(.22,.61,.36,1),
                           transform .38s cubic-bezier(.22,.61,.36,1)}
.lightbox-info{transition:opacity .32s cubic-bezier(.22,.61,.36,1)}
.lightbox.is-swapping .lightbox-media{opacity:0;transform:translateX(var(--lb-out,0px))}
.lightbox.is-swapping .lightbox-info{opacity:0}
/* One frame with no transition, placing the incoming work off to the far side
   before it animates home. */
.lightbox.is-entering .lightbox-media{transition:none;opacity:0;transform:translateX(var(--lb-in,0px))}
.lightbox.is-entering .lightbox-info{transition:none;opacity:0}
@media (prefers-reduced-motion: reduce){
  .lightbox-media,.lightbox-info{transition:none}
  .lightbox.is-swapping .lightbox-media{transform:none}
}

/* Same ink as the two arrows beside it. While the close mark was a text
   glyph its lighter --muted colour hid the weight mismatch; with all three
   drawn at 1px, a paler third box just reads as a rendering fault. */
.lightbox-close{flex:0 0 auto;width:38px;height:38px;display:flex;align-items:center;justify-content:center;color:var(--ink);background:transparent;border:1px solid var(--line);border-radius:3px;cursor:pointer;
  transition:background .35s cubic-bezier(.22,.61,.36,1),color .35s cubic-bezier(.22,.61,.36,1)}
@media (hover:hover){ .lightbox-close:hover{background:var(--ink);color:var(--white)} }

/* Prev / next — quiet hairline squares in the control bar, never on the art. */
/* Was a 2.3rem serif glyph with a padding nudge to fake optical centring —
   which is why the arrows outweighed the close button beside them. A drawn
   chevron centres itself and needs no font at all. */
.lb-nav{flex:0 0 auto;width:38px;height:38px;border-radius:3px;display:flex;align-items:center;justify-content:center;
  color:var(--ink);background:transparent;border:1px solid var(--line);cursor:pointer;
  transition:background .35s cubic-bezier(.22,.61,.36,1),color .35s cubic-bezier(.22,.61,.36,1),opacity .35s ease}
@media (hover:hover){ .lb-nav:hover{background:var(--ink);color:var(--white)} }

.lightbox-media{position:relative;flex:0 1 auto;min-height:0;overflow:hidden;background:#f3f0ea;display:flex;align-items:center;justify-content:center;padding:clamp(18px,3vw,34px);cursor:zoom-in;touch-action:manipulation}
/* While zoomed the image is dragged to pan, so the browser must not claim the
   gesture for scrolling — and the transform is driven from JS, not CSS.
   `touch-action` is NOT inherited and is read from the element actually
   touched, which is the <img>, not this wrapper. Setting it only here left the
   image on `touch-action:auto`, so iOS claimed the drag for scrolling and fired
   pointercancel — the pan simply died on touch devices. It must be on BOTH. */
/* The media panel opens the full-screen stage; it no longer zooms in place. */
/* "Watch the film" inside the catalogue entry. Quiet on purpose: the work
   itself is the subject of that panel, and the film is an extra way to look
   at it, not a competing call to action. It sits under the year, above the
   specification, because that is where a visitor stops reading and starts
   deciding whether they want to see more. */
/* ------------------------- Extra views ------------------------------
   A row of small plates under the film control, for the works that have
   more than one image. Square, hairline, no captions — they are a way back
   to a picture, not content in their own right, and numbering or labelling
   them ("verso", "detail") would invite reading a strip meant to be
   glanced at. The current one is marked by an ink border, which is the
   only weight in the row. */
.lb-views{display:flex;flex-wrap:wrap;gap:.5rem;margin:.9rem 0 .2rem}
.lb-views[hidden]{display:none}
.lb-view{width:52px;height:52px;padding:3px;background:var(--white);
  border:1px solid var(--line);cursor:pointer;flex:0 0 auto;
  display:flex;align-items:center;justify-content:center;overflow:hidden;
  transition:border-color .3s ease,opacity .3s ease}
.lb-view img{max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain}
.lb-view.is-current{border-color:var(--ink)}
@media (hover:hover){
  .lb-view{opacity:.78}
  .lb-view:hover{opacity:1;border-color:var(--gold)}
  .lb-view.is-current{opacity:1}
}
.lb-view:focus-visible{outline:2px solid var(--accent);outline-offset:2px;opacity:1}
/* Thumb-sized on touch, where 52px is on the small side of comfortable. */
@media (hover:none){
  .lb-view{width:58px;height:58px}
}

/* "Watch the film" in the catalogue entry.

   Two failed versions before this one, and both failures are instructive:
   an underlined word was missed entirely, and a bordered button was seen but
   read as pasted on — a box is the only box in a panel made of hairline rows
   and text, so it floats no matter how it is coloured.

   This version has NO box. Visibility comes from the ring, which is a shape
   nothing else in the panel uses, and the label sits on the same left rhythm
   and in the same face as the specification below it. It reads as part of the
   entry, and it is still the first thing the eye lands on under the year.
   Not gold: gold is the enquiry. */
.lb-film{margin:.8rem 0 .3rem}
.lb-film[hidden]{display:none}
.lb-film-btn{display:inline-flex;align-items:center;gap:.58rem;
  font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.18em;
  text-transform:uppercase;color:var(--ink);background:none;border:0;
  /* Padding is the tap target, the negative margin keeps the label flush with
     the column edge — the control must line up with MEDIUM below it. */
  padding:.42rem .3rem;margin-left:-.3rem;cursor:pointer;
  transition:color .3s ease}
.lb-film-mark{display:inline-flex;align-items:center;justify-content:center;
  width:17px;height:17px;border:1px solid var(--line);border-radius:50%;
  flex:0 0 auto;color:var(--muted);
  transition:color .3s ease,border-color .3s ease,transform .3s ease}
.lb-film-mark .icon{position:relative;left:1px}
@media (hover:hover){
  .lb-film-btn:hover{color:var(--gold)}
  .lb-film-btn:hover .lb-film-mark{color:var(--gold);border-color:var(--gold);transform:scale(1.08)}
}
.lb-film-btn:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:2px}
.lb-film-btn:focus-visible .lb-film-mark{color:var(--gold);border-color:var(--gold)}
/* A borderless control still needs a thumb-sized target. */
@media (hover:none){
  .lb-film{margin:.7rem 0 .25rem}
  .lb-film-btn{min-height:44px;padding-block:.6rem}
}

/* The card badge. Its whole job is to say "there is more here" before the
   work is opened — without it the film is invisible until someone clicks a
   work they had no reason to think was different. Mirrors .work-zoom's
   position language but sits on the LEFT so the two never collide. */
.work-film-flag{position:absolute;bottom:6px;left:6px;display:inline-flex;align-items:center;gap:.36rem;
  font-family:var(--label);font-size:.54rem;letter-spacing:.18em;text-transform:uppercase;
  color:var(--muted);background:var(--white);border:1px solid var(--line);
  height:30px;padding:0 .55rem;pointer-events:none}
/* Matches .work-zoom's 30px height exactly, so the two glyphs at the bottom of
   a card sit on one line rather than at two different heights. */
.work-film-flag .lb-film-mark{width:13px;height:13px}
/* Unlike .work-zoom this stays put on hover: it is information, not a control,
   and blinking it in and out would read as a glitch. */

.lb-open-stage{position:absolute;left:50%;bottom:clamp(10px,2vw,16px);transform:translateX(-50%);
  display:inline-flex;align-items:center;gap:.45rem;
  font-family:var(--label);font-size:var(--fs-label-xs);letter-spacing:.18em;text-transform:uppercase;
  color:var(--muted);background:var(--white);border:1px solid var(--line);border-radius:2px;
  padding:.5rem .9rem;cursor:pointer;opacity:.92;
  transition:color .3s,border-color .3s,opacity .3s}
/* On a touch device the whole media panel already opens the stage (see
   lbMedia's click handler) and the bar underneath says "Tap for full screen",
   so this button is a second way to do the same thing — and it sits ON the
   artwork, which is the one thing this dialog is not supposed to do. Pointer
   devices keep it: there it appears on hover, over the mount rather than
   permanently over the work. */
@media (hover:none){
  .lb-open-stage{display:none}
}
@media (hover:hover){
  .lb-open-stage{opacity:0}
  .lightbox-media:hover .lb-open-stage,.lb-open-stage:focus-visible{opacity:1}
  .lb-open-stage:hover{color:var(--ink);border-color:var(--ink)}
}
.lb-open-stage:focus-visible{outline:2px solid var(--accent);outline-offset:2px;opacity:1}
.lightbox-media img{max-width:100%;max-height:34vh;width:auto;height:auto;object-fit:contain;filter:drop-shadow(0 3px 16px rgba(0,0,0,.22));
  transition:transform .6s cubic-bezier(.22,.61,.36,1)}
.lightbox-media.is-zoomed img{filter:none}
/* Own row, below the counter and controls — see the note on .lb-bar. */
.lb-zoom-hint{order:3;flex:1 0 100%;
  font-family:var(--label);font-size:.58rem;letter-spacing:.22em;text-transform:uppercase;
  color:var(--muted);margin-left:.9rem;opacity:.85;transition:opacity .25s ease;white-space:nowrap}
/* Say "tap" on touch and "click" on pointer devices, without a line of JS —
   and once zoomed, say the thing that is actually useful: it can be dragged. */
.lb-zoom-hint::before{content:attr(data-touch)}
.lb-zoom-hint.is-pan::before{content:attr(data-touch-pan)}
@media (hover:hover){
  .lb-zoom-hint::before{content:attr(data-pointer)}
  .lb-zoom-hint.is-pan::before{content:attr(data-pointer-pan)}
}
.lightbox.is-zoomed .lb-zoom-hint{opacity:1;color:var(--accent)}

/* position + z-index so the media panel — which is `position:relative`, and
   therefore paints above static siblings — can never draw over this column. */
/* 2rem, not 2.4: the panel's own padding was 43px top and bottom on a large
   screen — 86px of a fixed-height column spent on air while the last rows of
   the specification were being pushed past the fold. */
.lightbox-info{padding:clamp(1.4rem,2.6vw,2rem);display:flex;flex-direction:column;min-height:0;
  position:relative;z-index:1;background:var(--white)}
/* Only the details body may scroll; the CTA lives outside it and is pinned. */
.lb-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain}
/* The specification grew (edition, signature, frame, condition, authentication,
   price) and now scrolls on most screens — so the last visible row was being
   cut mid-word with nothing to say the entry continued. This fades the final
   34px out instead of slicing it, which reads as "there is more" rather than
   "it ends here". main.js adds .has-more only while there is actually
   something below, so a fully-scrolled panel is never dimmed for no reason. */
.lb-scroll.has-more{-webkit-mask-image:linear-gradient(to bottom,#000 calc(100% - 34px),transparent);
  mask-image:linear-gradient(to bottom,#000 calc(100% - 34px),transparent)}
/* Control bar. Every lightbox control lives here rather than floating over the
   artwork: on phones it sits directly under the image, on desktop it is the top
   row of the right-hand panel — which is also the dialog's top-right corner, so
   Close still lands where a Close button belongs. */
/* The bar WRAPS, and the hint has its own line.
   It used to be one nowrap row — counter + "click for full screen" + three
   38px buttons — which needs about 400px. In a 380px column with 43px of
   padding it had 294px, so it overflowed by 63px. `.lightbox-info` is
   `overflow:hidden`, which makes it a scroll container, and `lbClose.focus()`
   on open scrolled the close button into view: the panel scrolled 63px
   sideways and took every line of the catalogue entry with it, under the
   media panel, which paints on top because it is positioned and this is not.
   That is the "it's hiding the details on the right" bug, and it is why it
   appeared the moment the lightbox opened rather than on resize. */
.lb-bar{display:flex;flex-wrap:wrap;align-items:center;gap:.4rem;row-gap:.55rem;
  margin-bottom:1.05rem;min-width:0}
.lb-controls{display:flex;align-items:center;gap:.4rem;margin-left:auto;flex:0 0 auto}
/* Visible gap so Close is never mistaken for (or mis-tapped as) Next. */
.lightbox-close{margin-left:.75rem}
.lb-counter{font-family:var(--label);font-size:.58rem;letter-spacing:.24em;text-transform:uppercase;color:var(--muted);margin:0;flex:0 0 auto}
.lightbox-info .work-title{font-size:clamp(1.5rem,3vw,1.95rem);line-height:1.15;margin:0 0 .25rem}
/* Parenthetical/alternate title — quieter second line, so repeated words
   ("3D Rat (Rat with 3D Glasses…)") don't read as a shouty block. */
.lb-subtitle{font-family:var(--body);font-style:italic;font-size:1rem;line-height:1.35;color:var(--muted);margin:0 0 .7rem}
/* 1rem, not 1.5: the gap between the year and the first specification row was
   pure air in a panel whose last rows were being pushed out of sight. */
.lightbox-info .lb-year{font-family:var(--label);font-size:.66rem;letter-spacing:.2em;text-transform:uppercase;color:var(--muted);margin:0 0 1rem}

/* Catalogue specification — labelled rows on hairlines, the way a gallery or
   auction house presents a work. Stacked (label above value) because the info
   column is narrow; far more legible than an unlabelled stack of lines. */
.lb-detail{margin:0 0 1.9rem}
.spec{margin:0;border-top:1px solid var(--line)}
.spec-row{padding:.8rem 0;border-bottom:1px solid var(--line)}
.spec dt{font-family:var(--label);font-size:.56rem;letter-spacing:.22em;text-transform:uppercase;color:var(--muted);margin:0 0 .3rem}
.spec dd{margin:0;color:var(--ink);font-size:.95rem;line-height:1.55}
.spec-alt{display:block;color:var(--muted)}

/* ---- The specification, set as a catalogue ----------------------------
   Stacked (label above value) the entry ran to ~460px and pushed PRICE — the
   row a buyer looks for — below the fold of the lightbox panel. A label ABOVE
   its value costs a whole line per row for a word that is usually shorter than
   the value beside it.

   Set in two columns instead, as an auction catalogue sets it: label left,
   value right, both on the same baseline. That is ~27px saved on every row and
   about 190px over the entry, which is the difference between the whole
   specification being visible and the last two rows being hidden.

   The label column is 108px: AUTHENTICATION measures ~101px at this size and
   tracking, so there is a few pixels of headroom rather than an exact fit.
   Add a longer label and check it still sits on ONE line — a wrapped label
   puts back exactly the height this removes.

   Below 720px it stays stacked: the panel is the full width of a phone and
   there is not room for two columns without the values wrapping to ribbons. */
@media(min-width:720px){
  .spec-row{display:grid;grid-template-columns:108px 1fr;gap:0 1.1rem;
    align-items:baseline;padding:.42rem 0}
  .spec dt{margin:0;font-size:.54rem;letter-spacing:.13em;line-height:1.6}
  .spec dd{line-height:1.45}
  /* Inches beside the centimetres rather than under them — one measurement,
     one line. The separator is a middot with air, not a slash. */
  .spec-alt{display:inline;margin-left:.55rem}
  .spec-alt::before{content:"·";margin-right:.55rem;color:var(--line)}
}

/* CTA sits at the foot of the panel; sized to its label so it never wraps. */
.lb-cta{flex:0 0 auto;align-self:flex-start;white-space:nowrap;margin-top:1.4rem}
/* Short viewports (most laptops): tighten the panel so the whole entry —
   including the CTA — fits without any scrolling. */
@media(max-height:820px){
  .lightbox-info{padding:clamp(1.1rem,2.2vw,1.6rem)}
  .lightbox-info .work-title{font-size:clamp(1.25rem,2.2vw,1.5rem)}
  .lb-subtitle{font-size:.92rem;margin-bottom:.5rem}
  .lightbox-info .lb-year{margin-bottom:.9rem}
  .lb-counter{margin-bottom:.7rem}
  .spec-row{padding:.55rem 0}
  .spec dd{font-size:.9rem;line-height:1.45}
  .lb-detail{margin-bottom:1rem}
  .lb-cta{margin-top:.7rem}
  /* Short laptops: 4px off each row is another ~30px, which is the whole
     difference between the entry fitting and Price sitting under the fade. */
  .spec-row{padding:.4rem 0}
  .lightbox-media img{max-height:40vh}
}
@media(min-width:820px) and (max-height:820px){
  .lightbox-media img{max-height:70vh}
}
/* ---- Stacked layout (phones): the artwork must never be cropped ----
   The bug this fixes: the image's height was capped in `vh` (34/40vh) while
   the panel around it was `flex:0 1 auto` — so when the details column needed
   room, the PANEL shrank and the IMAGE did not, and `overflow:hidden` sliced
   the top and bottom off the work. Measured at 390x750: panel 272px, image
   300px, 14px of the artwork gone at each end.

   The fix is to stop sizing the image against the viewport and size the PANEL
   instead: a definite height here means `max-height:100%` on the image
   resolves against the box it actually sits in, so the artwork is always
   whole. `svh`, not `vh` — on iOS `vh` is the viewport with the browser chrome
   hidden, which is not what the visitor is looking at.

   Tighter padding than the desktop mount (12px rather than 18-34px): on a
   phone every pixel of margin is a pixel not spent on the work. */
@media(max-width:819px){
  .lightbox-media{flex:0 0 auto;padding:12px;
    height:clamp(200px,46vh,460px);height:clamp(200px,46svh,460px)}
  .lightbox-media img{max-height:100%;max-width:100%}
}
body.lb-open{overflow:hidden}
@media(min-width:820px){
  .lightbox-dialog{flex-direction:row}
  .lightbox-media{flex:1 1 auto;min-width:0;min-height:0}
  /* 82vh, up from 78. On a tall screen the dialog is sized by the MEDIA
     column, so those four extra vh give the info column four more too — which
     is what lets the whole specification sit above the fold there. The
     artwork gets bigger and the entry stops being cut: the same change buys
     both. Short viewports are governed by the max-height block below. */
  .lightbox-media img{max-height:82vh}
  /* 420, not 380: the extra 40px is what gives the specification room to sit
     on one line each. The dialog grew by the same amount (see max-width), so
     the artwork itself is unchanged — it is height-capped at 78vh, and the
     media panel simply has more mount around it. */
  /* No overflow-y here. This column had TWO scroll containers — itself and
     .lb-scroll inside it — and the outer one was the reason the entry was
     being cut on a tall screen: with `overflow-y:auto` the column stops
     asking the flex row for the height its content needs, so the dialog was
     sized by the MEDIA column and the specification scrolled inside whatever
     was left. Without it the dialog grows to fit the entry (up to its 92svh
     cap) and .lb-scroll — which exists for exactly this — takes over only
     when the viewport really is too short. */
  .lightbox-info{flex:0 0 420px;min-width:0;min-height:0;overflow-x:hidden}
}

/* ------------------- Field hint (e.g. dialling code) --------------- */
.field-hint{display:block;font-family:var(--label);font-size:.66rem;letter-spacing:.02em;text-transform:none;color:var(--muted);margin-top:.35rem;line-height:1.4}

/* ------------------- Private Advisory film ------------------------- */
.advisory-film{margin-top:1.8rem}
.advisory-film-intro{font-family:var(--label);font-size:.74rem;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:0 0 .8rem}
.video-embed--placeholder{display:flex;align-items:center;justify-content:center}
.video-embed--placeholder span{font-family:var(--label);font-size:.72rem;letter-spacing:.14em;text-transform:uppercase;color:#cfc9bd}

/* Enquiry anchor lands on the form itself (esp. mobile, where the Private
   Advisory column stacks above the form). Offset for the sticky header. */
#enquire{scroll-margin-top:88px}

/* ---------------- Lenis smooth scroll (v1.3.26) --------------------
   Whole-page inertia scrolling on desktop for a premium, eased feel between
   sections. Required Lenis rules (self-hosted; class `lenis` is added to
   <html> by the library at init). Touch is left native (Lenis smooths the
   wheel only), and it isn't initialised at all under reduced-motion — see
   main.js. The CSS proximity scroll-snap that used to live here was removed:
   it conflicts with Lenis, which now provides the section-to-section glide. */
html.lenis,html.lenis body{height:auto}
.lenis.lenis-smooth{scroll-behavior:auto!important}
.lenis.lenis-smooth [data-lenis-prevent]{overscroll-behavior:contain}
.lenis.lenis-stopped{overflow:clip}
.lenis.lenis-smooth iframe{pointer-events:none}

/* ==================== The three signature entrances ==================
   The page already fades its sections in on scroll. These three are the ones
   that are supposed to be NOTICED — the headline, the hero picture, and the
   rule under every section heading — and they exist because a luxury page is
   not the one with the most motion, it is the one where the few moves it makes
   are slow, deliberate and always the same.

   Everything here is inside `html.reveal-on`, so under reduced-motion or with
   JS off none of it is applied and the page is simply, immediately, there.

   1 — THE HEADLINE RISES, LINE BY LINE.
   Each line sits in a box with `overflow:hidden` and slides up from beneath its
   own edge. This is the one gesture on the page that says "couture" rather than
   "website", and it costs nothing: the words are not split into per-character
   spans, so the h1 is still one string to a screen reader and to Google.
   The padding/negative-margin pair is not decoration — without it the clipping
   box cuts the descender off the "g" in Collecting. */
.hero-copy h1 .line{display:block;overflow:hidden;
  padding-bottom:.1em;margin-bottom:-.1em}
html.reveal-on .hero-copy h1 .line>span{display:block;transform:translateY(115%);
  transition:transform 1.15s cubic-bezier(.22,.61,.36,1)}
html.reveal-on .hero-copy h1.is-visible .line>span{transform:none}
/* The second line follows the first rather than arriving with it. 130ms is the
   whole trick: long enough to read as two movements, short enough that it is
   never a queue. */
html.reveal-on .hero-copy h1 .line:nth-child(2)>span{transition-delay:.13s}

/* 2 — THE PICTURE IS UNCOVERED, NOT FADED IN.
   A sheet of the page's own paper slides off the artwork — the same gesture as
   the load veil, at the scale of one object — while the image itself settles
   from 1.06 to 1. The settle is the half of it that does the work: a picture
   that arrives already still looks pasted on; one that comes to rest looks
   like it was placed. */
html.reveal-on .hero-media{overflow:hidden}
html.reveal-on .hero-media::after{content:"";position:absolute;inset:0;z-index:2;
  background:var(--bg);transform:translateX(0);
  transition:transform 1.15s cubic-bezier(.22,.61,.36,1)}
html.reveal-on .hero-media.is-visible::after{transform:translateX(101%)}
html.reveal-on .hero-media img{transform:scale(1.06);
  transition:transform 1.9s cubic-bezier(.22,.61,.36,1)}
html.reveal-on .hero-media.is-visible img{transform:none}

/* 3 — EVERY SECTION HEADING DRAWS ITS OWN RULE.
   56px of gold under the heading, drawn from the centre out as the section
   arrives. Repeated six times down the page it becomes the site's signature —
   the same argument as the hairline everything else is built from.

   Absolutely positioned INSIDE the heading's existing 50px bottom margin, so
   it adds no height and the section rhythm set elsewhere is untouched. Change
   `bottom` if that margin ever changes. */
.section-head{position:relative}
.section-head::after{content:"";position:absolute;left:50%;bottom:-25px;
  width:56px;height:1px;background:var(--gold);
  transform:translateX(-50%) scaleX(1);transform-origin:center;
  transition:transform 1.15s cubic-bezier(.22,.61,.36,1) .18s}
html.reveal-on .section-head::after{transform:translateX(-50%) scaleX(0)}
html.reveal-on .section-head.is-visible::after{transform:translateX(-50%) scaleX(1)}

/* ------------------------- The load veil ---------------------------
   Not a preloader. There is no spinner, no percentage and no waiting on
   assets — a progress indicator on a six-page landing page is theatre, and it
   makes the site feel slower than it is. This is one sheet of the page's own
   paper laid over the top and lifted: the page is REVEALED rather than
   assembled in front of the visitor.

   Why a veil over the page rather than `body{opacity:0}`:
   - An opacity on <body> creates a containing block for `position:fixed`
     children, which would strand the header and the sticky bar mid-animation.
   - It would also compound with the scroll-reveal opacities underneath it, so
     the hero would fade in twice, at two different rates.
   The veil touches nothing but itself.

   `pointer-events:none` from the first frame: even mid-fade it can never
   swallow a tap. Rendered only when <html> carries `veil-on` — with JS off or
   under reduced-motion the element is `display:none` and the page is simply
   there. main.js removes the node once it has finished, so nothing fixed is
   left in the document. */
.page-veil{display:none}
html.veil-on .page-veil{display:block;position:fixed;inset:0;z-index:900;
  background:var(--bg);pointer-events:none;opacity:1;
  /* Slow out, no ease in — a curtain that starts fast and settles reads as
     mechanical. Same easing curve as every other transition on the site. */
  transition:opacity .8s cubic-bezier(.22,.61,.36,1)}
html.veil-on.is-lifted .page-veil{opacity:0}

/* ---------------- Scroll reveals (elegant fade + rise) --------------
   Targets are pre-hidden ONLY when <html> carries `.reveal-on` (added by the
   head bootstrap when motion is allowed and JS is on); main.js adds
   `.is-visible` as each element enters view, staggering grouped items. Without
   JS, or under reduced-motion, `.reveal-on` is never set — nothing is hidden,
   so content and SEO are unaffected. Keep this selector list in sync with the
   one in main.js. */
/* .hero-copy h1 and .hero-media are NOT in this list — they are still
   observed by main.js (keep the JS selector in sync) but they get their own
   entrances further down: the headline rises line by line, the picture is
   uncovered by a wipe. A generic fade on top of either would be two entrances
   fighting for the same second. */
html.reveal-on .hero-kicker,
html.reveal-on .hero-lead,
html.reveal-on .hero-copy .btn,
html.reveal-on .section-head,
html.reveal-on .section-intro,
html.reveal-on .about-copy,
html.reveal-on .why-copy,
html.reveal-on .why-media,
html.reveal-on .press,
html.reveal-on .voice-viewport,
html.reveal-on .work,
html.reveal-on .trust-grid>div,
html.reveal-on .timeline-frame,
html.reveal-on .faq-item,
html.reveal-on .sourcing-cta,
html.reveal-on .enquire-intro{
  opacity:0;transform:translateY(24px);
  transition:opacity 1.05s cubic-bezier(.22,.61,.36,1),transform 1.05s cubic-bezier(.22,.61,.36,1);
}
html.reveal-on .is-visible{opacity:1!important;transform:none!important}

/* Above 720px the exhibition timeline becomes a horizontal scroll box, and
   per-card reveals do not work inside one. Two separate failures:

   1. Cards that start off to the right are clipped by the scroller, so they
      never intersect the viewport and sit at `translateY(24px)` indefinitely.
      Transformed overflow counts towards scrollable overflow, which made the
      box 17px taller than its frame and turned it into a VERTICAL scroll
      container — park the wheel over it and the page stalls. That is the
      "page gets stuck on the timeline" bug.
   2. Jumping the strip (a dot, or a fast flick) can carry cards past the
      observer without it ever sampling them, leaving them permanently blank.

   So above 720px the cards are simply shown, and `.timeline-frame` reveals as
   one unit instead. Below 720px the timeline is an ordinary vertical list with
   no clipping ancestor, so it keeps the per-card stagger.

   This block sits AFTER the rules it overrides on purpose: same specificity
   means source order decides, and placing it earlier silently loses — the trap
   that has now caught four rules on this page. */
/* The exhibition cards are NOT in the reveal list any more — `.timeline-frame`
   reveals as one unit and the cards handle their own opacity below.

   They had to come out. The reveal system's `html.reveal-on .is-visible
   {opacity:1 !important}` locks a revealed element at full opacity, which
   silently beat the fade: the JS was setting --tl-in correctly and the strip
   simply never faded. Removing the cards from the observer removes the
   conflict at its root rather than answering one !important with another.
   It also retires the old `@media(min-width:720px){...opacity:1}` patch that
   existed only to stop off-screen cards being stuck invisible. */
@media(min-width:720px){
  /* A card's opacity follows --tl-in — how much of it is inside the frame,
     set by main.js on every scroll frame. A card arriving at the edge fades up
     instead of sliding in hard, and the one leaving fades out instead of being
     sliced by the frame edge. That is what makes the auto-advance read as a
     deliberate effect rather than a jerk.
     Defaults to 1, so with JS off every card is fully opaque. */
  .tl-item{opacity:calc(.18 + .82 * var(--tl-in,1));
    transition:opacity .45s cubic-bezier(.22,.61,.36,1)}

  /* The loop is a cut, not a rewind: the strip fades out, jumps back to the
     first card behind the fade, and fades in. Smooth-scrolling back past
     twelve cards would read as a mistake. */
  .timeline{transition:opacity .4s cubic-bezier(.22,.61,.36,1)}
  .timeline-frame.is-rewinding .timeline{opacity:0}
}
@media (prefers-reduced-motion: reduce){
  .tl-item,.timeline{transition:none}
}

/* Hero: a gentle, ordered cascade so the whole first view settles in as ONE
   composed picture (kicker → headline/media → lead → button) rather than each
   piece popping in on its own. Small delays, shared easing. */
html.reveal-on .hero-kicker{transition-delay:.05s}
html.reveal-on .hero-media{transition-delay:.12s}
html.reveal-on .hero-copy h1{transition-delay:.16s}
html.reveal-on .hero-lead{transition-delay:.30s}
html.reveal-on .hero-copy .btn{transition-delay:.42s}
