/* The device layer. Measurements in comments came from the era-residence study;
   see docs/reference-studies/era-residence/BREAKDOWN.md */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--ground); color: var(--ink);
  font-family: var(--body); font-size: clamp(15px, 1.05vw, 17px);
  line-height: 1.55; overflow-x: hidden;
}
img { display: block; max-width: 100%; }
a { color: inherit; }

/* ------------------------------------------------------------------ type --- */
.display {
  font-family: var(--display); font-weight: var(--display-weight, 400);
  line-height: 0.92; letter-spacing: var(--display-track, -0.02em);
  text-wrap: balance; margin: 0;
}
/* Label and value in different FACES, not the same face shrunk. */
.label {
  font-family: var(--body); font-weight: 700;
  font-size: clamp(9px, 0.72vw, 11px); letter-spacing: 0.22em;
  text-transform: uppercase; margin: 0 0 1.1rem;
  /* dim by MIXING WITH THE CURRENT COLOUR, never by naming a colour. A fixed
     --ink-dim is derived for one ground, and every section added since then
     put light-ground brown on a dark photograph: the gallery's statement was
     rgb(89,79,73) on a mid-grey sky. */
  color: color-mix(in srgb, currentColor 70%, transparent);
}
/* The same display family at the opposite end of the tracking scale. The
   reference runs one Didone at +18px and at -2px; the range is the point. */
.tracked {
  font-family: var(--display); font-size: clamp(11px, 0.85vw, 13px);
  letter-spacing: 0.42em; text-transform: uppercase;
}
.lede { max-width: 34ch; color: color-mix(in srgb, currentColor 70%, transparent); }

/* --------------------------------------------------------------- reveals --- */
/* Per character: a real rotateY flip. Measured peak on the reference was
   -87.09deg at 2.2px glyph width and opacity 0.03, rising 52px as it turns. The
   glyph widens as it rotates toward the viewer; that width is not animated, it
   falls out of the rotation. */
[data-reveal] { perspective: 900px; }
.w { display: inline-block; white-space: nowrap; }
/* INLINE, not inline-block. An inline-block word is measured with different
   metrics from the plain text run it is rebuilt as, so a line that just fitted
   during measurement is a hair too wide when painted and its last word drops to
   a line of its own: "of" and "call" each sitting alone under a full line. */
.probe { display: inline; }

/* Slow. The previous timing read as rushed and unpolished.
   Every reveal below is written twice: once for .in and once for .out, and the
   exit is the entrance inverted. A block that animates in and then simply
   disappears when it leaves reads as half-built. */
/* Exactly edge on, and fully transparent, until the flip has turned it far
   enough to be a letter. At -87deg with 3% opacity every glyph left a faint
   vertical hairline on the page before its turn came: the edge of the card was
   visible before the card moved. The opacity is held back a third of a second
   behind the rotation so nothing appears until it is already turning. */
.c {
  display: inline-block;
  transform: rotateY(-90deg) translateY(52px); opacity: 0;
  transition: transform 1.15s cubic-bezier(.16,1,.3,1),
              opacity .5s ease calc(var(--i) * 45ms + .3s);
  transition-delay: calc(var(--i) * 45ms);
  will-change: transform, opacity;
}
.in .c { transform: none; opacity: 1; }
/* out: the flip continues in the same direction rather than reversing, so it
   reads as the type turning away rather than rewinding */
.out .c { transform: rotateY(84deg) translateY(-42px); opacity: 0;
          transition: transform 1.15s cubic-bezier(.16,1,.3,1),
                      opacity .45s ease calc(var(--i) * 45ms); }

/* A DIFFERENT reveal for the line beneath a title: letters fade in and slide in
   from the RIGHT. The masthead and its subtitle must never share an animation,
   which is what made the small line look like an afterthought. */
[data-reveal="chars-right"] .c {
  transform: translateX(46px); opacity: 0;
  transition: transform 1.25s cubic-bezier(.2,.9,.25,1), opacity 1.05s ease;
  transition-delay: calc(var(--i) * 52ms);
}
[data-reveal="chars-right"].in .c { transform: none; opacity: 1; }
[data-reveal="chars-right"].out .c { transform: translateX(-46px); opacity: 0; }

/* A third: letters rise from behind their own line, no flip. For labels. */
[data-reveal="chars-up"] .c {
  transform: translateY(90%); opacity: 0;
  transition: transform 1.1s cubic-bezier(.16,1,.3,1), opacity .9s ease;
  transition-delay: calc(var(--i) * 38ms);
}
[data-reveal="chars-up"].in .c { transform: none; opacity: 1; }
[data-reveal="chars-up"].out .c { transform: translateY(-90%); opacity: 0; }

/* PER LINE, and the lines are the ones the browser actually drew, not the ones
   a <br> happened to mark. Each line rises into place from below its own line
   box and fades as it comes; on the way out it keeps going up and fades away.
   Cascade order is top to bottom in both directions, which is reading order.
   No block of text longer than one line ever arrives all at once. */
/* The mask that lets a line slide up from behind itself is `overflow: hidden`
   on a box exactly one line-height tall. Any face whose INK is taller than its
   line box then gets clipped, and display leading below 1 guarantees it: the
   script sub-line on the cover was 104px in an 83px box, so its ascenders and
   descenders were sliced off. Pad the mask and pull the padding back out with a
   matching negative margin, so the clip is generous while the layout is
   unchanged. The horizontal padding is for the same reason: a script face's
   flourishes and the text shadow both overhang the advance width. */
.l { display: block; overflow: hidden;
     /* Room for ink that overhangs its line box: ascenders and descenders
        vertically, a script face's flourishes and the text shadow horizontally.
        The LEFT padding is off by default, because it scales with the element's
        own font size and therefore pushes a 37px statement 13px right while
        pushing the 10px label above it only 4px: two things that should share a
        left edge end up 9px apart. Centred blocks get it back symmetrically,
        where it cannot move the centre. */
     --lpad: 0px;
     padding: .4em .34em .4em var(--lpad); margin: -.4em 0; }
.masthead .l, .magtype .l, .mgtitles .l, .mgcaps .l, .end .l,
.mini-sec .label .l, .scene-open .l { --lpad: .34em; }
.li {
  display: block; transform: translateY(108%); opacity: 0;
  transition: transform 1.15s cubic-bezier(.16,1,.3,1),
              opacity .85s cubic-bezier(.16,1,.3,1);
  transition-delay: calc(var(--i) * 95ms);
}
.in .li { transform: none; opacity: 1; }
.out .li {
  transform: translateY(-108%); opacity: 0;
  transition-delay: calc(var(--i) * 55ms);      /* leaves a little quicker */
}

[data-fade] { opacity: 0; transform: translateY(22px);
  transition: opacity 1.2s ease, transform 1.2s cubic-bezier(.16,1,.3,1); }
[data-fade].in { opacity: 1; transform: none; }
[data-fade].out { opacity: 0; transform: translateY(-22px); }

[data-stagger] > *, .hrs tr, .endmark { transition-property: opacity, transform; }
[data-stagger].out > * { opacity: 0; transform: translateY(-26px); }
.hrs.out tr { opacity: 0; transform: translateY(-20px); }
.endmark.out { opacity: 0; transform: translateY(-14px); }

@media (prefers-reduced-motion: reduce) {
  .c, .li, [data-fade] { transform: none !important; opacity: 1 !important; transition: none !important; }
}

/* ------------------------------------------------------------ ambient pan --- */
/* Every framed photograph moves inside its own frame at all times, measured at a
   constant 60px/s. The cheapest answer to "it looks like a slideshow", and the
   one thing here that is deliberately LINEAR: everything else decelerates. */
/* The frame needs its own intrinsic height. Once the image inside became
   absolutely positioned so it could travel, it left the flow and every frame
   collapsed to zero height, which silently disabled the parallax that was
   computed against it. */
.frame { overflow: hidden; position: relative; aspect-ratio: 4 / 3; }
.frame.v { aspect-ratio: 3 / 4; }
.frame.wide { aspect-ratio: 16 / 9; }
/* frames that are stretched by their parent supply their own size */
.shots > .shot .frame, .pane .frame, .crop .frame { aspect-ratio: auto; }
/* The drift animates `translate` and the gallery zoom animates `scale`: two
   separate CSS properties that compose. Both on `transform` meant the animation
   always beat the transition, so the gallery zoom never rendered at all. */
/* Oversized on purpose: the image TRAVELS inside its frame, which is the
   reference's own device ("the image moves within its bounding box"). 106% left
   only 6% of headroom, so any visible travel would have exposed the frame edge.
   150% height gives 25% of headroom top and bottom.
   The drift animates `translate` and the parallax writes `transform`, so both
   run on the same element without one cancelling the other. */
/* OVERSCAN, and a drift that stays inside it.
   The image used to sit at -3% and be 106% wide, then drift 5% sideways: at the
   end of the drift it was 2% short of the frame's far edge and the frame's own
   background showed through. Every picture on the site that was supposed to
   bleed to an edge had a hairline of ground down one side, and a full-bleed
   frame whose image was sized to exactly 100% had an 11px gap.
   6% of overscan on every side, drift capped at 3%, so coverage is guaranteed
   by construction rather than by choosing matching numbers by hand. */
.frame > img {
  position: absolute; top: -6%; left: -6%;
  /* max-width:100% is set on every img in the reset, and it silently caps this
     back to the frame's own width, so the horizontal overscan never existed and
     every drifting picture left a gap down one side. */
  width: 112%; max-width: none; height: 112%; object-fit: cover;
  animation: drift 26s linear infinite alternate;
}
@keyframes drift { from { translate: 2.6% 0; } to { translate: -2.6% 0; } }
.frame.v > img { animation-name: driftv; }
@keyframes driftv { from { translate: 0 2.6%; } to { translate: 0 -2.6%; } }
/* the parallax layer wraps the frame, so the image keeps its ambient drift and
   the two run at independent rates: exactly the layered motion the study calls
   for, rather than one cancelling the other */
.parbox { position: relative; will-change: transform; }
.frame[data-par] { will-change: transform; }
@media (prefers-reduced-motion: reduce) { .frame > img { animation: none; } }

/* --------------------------------------------------------- trapezoid wipe --- */
/* polygon(0 0, X 0, 1.25X 100%, 0 100%), X from 125% to 0%. The bottom point is
   always exactly 1.25x the top, so the wedge straightens as it shrinks and the
   edge appears to swing. One animated number does sweep and rotation together. */
.shots { position: relative; }
.shots > .shot { position: absolute; inset: 0; opacity: 0; pointer-events: none; }
/* The parallax wrapper is position:relative, so an absolutely positioned frame
   inside it resolves against a box with no height and collapses. Where the frame
   must fill its slot, the wrapper has to fill it too. */
.shots > .shot .parbox, .pane .parbox { position: absolute; inset: 0; }
.shots > .shot .frame, .pane .frame { position: absolute; inset: 0; }
.shot.on { opacity: 1; pointer-events: auto; z-index: 1; }
.shot.under { opacity: 1; z-index: 0; }
/* Registering --x makes the custom property itself animatable, so the wedge
   interpolates smoothly instead of the clip-path snapping between two states. */
@property --x { syntax: "<percentage>"; inherits: false; initial-value: 125%; }
/* the MOVE slot below supplies the clip-path; this only sets the shared state */
.shot.wiping { opacity: 1; z-index: 2; }
/* ===================== THE GALLERY FORMULA ==============================
   Every gallery transition runs THREE animations SIMULTANEOUSLY:

     1. MOVE      how the frame gets from one picture to the next
     2. OUTGOING  something done to the picture that is leaving
     3. INCOMING  something done to the picture that is arriving

   One alone reads as a slideshow. Three at once is what makes a gallery look
   expensive. Each slot is a variable: swap the option and the same formula
   produces a different gallery. Options must not collide on a CSS property, so
   MOVE owns clip-path, and OUTGOING/INCOMING own scale, rotate and filter.
   `translate` is reserved for the ambient drift and is never used here.

   MOVE      .gmove-wedge  .gmove-iris  .gmove-curtain  .gmove-diag  .gmove-arch
   OUTGOING  .gout-zoom    .gout-tilt   .gout-dim       .gout-shrink
   INCOMING  .gin-zoom     .gin-turn    .gin-bright     .gin-settle
   ======================================================================== */

/* All three animations share ONE duration and ONE curve so they are genuinely
   simultaneous, not merely overlapping. Rotation is gone entirely: it made the
   images look lopsided. The origin is pushed toward the side the wipe travels
   toward, so the zoom PULLS the picture across rather than just growing it. */
.shot > .frame > img {
  scale: 1; filter: none;
  transform-origin: 22% 50%;
  transition: scale 1.4s cubic-bezier(.33,0,.2,1),
              filter 1.4s cubic-bezier(.33,0,.2,1);
}

/* --- MOVE: the wedge, measured off the reference. X sweeps 125% to 0% and the
   bottom point is always 1.25x the top, so the edge straightens as it goes. */
.gmove-wedge .shot.wiping {
  clip-path: polygon(0 0, var(--x) 0, calc(var(--x) * 1.25) 100%, 0 100%);
}
/* --- MOVE: an iris closing on the centre */
.gmove-iris .shot.wiping { clip-path: circle(calc(var(--x) * 0.62) at 50% 50%); }
/* --- MOVE: a curtain falling from the top */
.gmove-curtain .shot.wiping { clip-path: inset(0 0 calc(100% - var(--x)) 0); }
/* --- MOVE: a diagonal from the top-right corner */
.gmove-diag .shot.wiping {
  clip-path: polygon(0 0, var(--x) 0, 0 calc(var(--x) * 1.15));
}
/* --- MOVE: an arch sweeping down, the same shape family as the page's arches */
.gmove-arch .shot.wiping {
  clip-path: inset(0 0 calc(100% - var(--x)) 0 round 0 0 46% 46% / 0 0 34% 34%);
}
.shot.wiping { transition: --x 1.4s cubic-bezier(.33,0,.2,1); }

/* --- OUTGOING: what happens to the picture that is leaving.
   Listed after .on because the outgoing slide carries both classes and, at equal
   specificity, the later rule wins. */
/* The arriving image is snapped into its start state with no transition, then
   released in the same frame the wipe begins, so all three run together. */
.shot.under > .frame > img { transition: none; }
/* The picture that is LEAVING zooms IN, toward the side the wipe is travelling
   to; the picture ARRIVING zooms OUT, from the side it is coming in on. The
   reverse of that (shrinking the one going away, growing the one arriving) reads
   backwards, and every option that scaled BELOW 1 also pulled the image inside
   its own frame and let the section colour show around it. Nothing here goes
   below 1. */
.gout-zoom   .shot.wiping > .frame > img, .gout-zoom   .shot.wiping.on > .frame > img { scale: 1.38; }
.gout-push   .shot.wiping > .frame > img, .gout-push   .shot.wiping.on > .frame > img { scale: 1.30; filter: brightness(.72); }
.gout-dim    .shot.wiping > .frame > img, .gout-dim    .shot.wiping.on > .frame > img { scale: 1.24; filter: brightness(.45) saturate(.65); }
.gout-lift   .shot.wiping > .frame > img, .gout-lift   .shot.wiping.on > .frame > img { scale: 1.44; filter: brightness(1.12); }

/* --- INCOMING: what happens to the picture that is arriving. It starts in the
   altered state under .under, then settles once it becomes .on. */
.gin-zoom   .shot.under > .frame > img { scale: 1.40; }
.gin-open   .shot.under > .frame > img { scale: 1.22; }
.gin-bright .shot.under > .frame > img { filter: brightness(1.7) saturate(.55); scale: 1.26; }
.gin-settle .shot.under > .frame > img { scale: 1.32; filter: brightness(.8); }

/* Released in the same frame the wipe begins, so all three animations run
   together rather than the arriving image playing afterwards. This MUST sit
   after the .gin-* block: the two selectors tie on specificity, so whichever is
   written last wins, and written first this rule did nothing at all. */
.shot.under.gsettle > .frame > img {
  transition: scale 1.4s cubic-bezier(.33,0,.2,1), filter 1.4s cubic-bezier(.33,0,.2,1);
  scale: 1; filter: none;
}

/* --- optional formula variation: the whole gallery leans toward the cursor */
/* The lean lives on a WRAPPER, not on the slides.
   Putting it on .shot meant `.gtilt .shots > .shot` (0,3,0) outranked
   `.shot.wiping` (0,2,0), and because `transition` is a shorthand it did not
   merely add the transform transition, it replaced the --x transition
   altogether. The wipe then had no transition at all and jumped from 125% to 0%
   in one frame: three of the seven galleries "had no real transition", and this
   was the whole reason. */
.gtilt .shots { perspective: 1100px; }
.gtilt .shots {
  transform: rotateX(calc(var(--py, 0) * -3deg)) rotateY(calc(var(--px, 0) * 4deg));
  transition: transform .5s cubic-bezier(.2,.8,.3,1);
  transform-style: preserve-3d;
}

/* gallery controls: arrow, index, timer, next index, arrow */
.gctl { display: flex; align-items: center; gap: 14px; margin-top: 20px; }
.garrow { background: none; border: 0; color: inherit; cursor: pointer; padding: 2px 4px;
          opacity: .55; font-size: 15px; line-height: 1; }
.garrow:hover, .garrow:focus-visible { opacity: 1; }
.gidx, .gnextnum { font-family: var(--body); font-weight: 700; font-size: 10px;
                   letter-spacing: .12em; font-variant-numeric: tabular-nums; opacity: .8; }
/* A horizontal version of the side rail. The old version put the fill and the
   track in the same colour under one opacity, so a full bar and an empty bar
   rendered identically: it looked static because it WAS invisible. Track and
   fill now carry their own alpha, and the bar is thick enough to read. */
.gtimer { position: relative; flex: 1; height: 2px; overflow: hidden;
          background: color-mix(in srgb, currentColor 22%, transparent); }
.gtimer i { position: absolute; inset: 0; display: block; background: currentColor;
            opacity: .95; transform-origin: 0 50%; transform: scaleX(0); }

/* ------------------------------------------------------------------ ring --- */
/* Not a solid ring: broken by two gaps with detached arcs just outside each
   break, and they rotate. The break is what stops it reading as generic. */
/* Two arcs, top and bottom, each with a travelling segment that runs its
   length clockwise on hover with a real speed curve. The previous version
   rotated a stray tick round the outside and lit the whole outline at once the
   instant the cursor arrived, which is the snap Aaron keeps objecting to. */
.ring { position: relative; display: grid; place-items: center;
        width: clamp(140px, 13vw, 190px); aspect-ratio: 1; border-radius: 50%;
        text-decoration: none; }
.ring svg { position: absolute; inset: 0; width: 100%; height: 100%;
            overflow: visible; pointer-events: none; }
.ring svg circle { fill: none; stroke: currentColor; stroke-width: 1;
                   transform-origin: 50% 50%; }
.ring .rtrack { opacity: .32; }
.ring .rrun {
  opacity: 0;
  stroke-dasharray: var(--seg) var(--gap);
  stroke-dashoffset: var(--seg);
  transition: opacity .5s ease, stroke-dashoffset 1.15s cubic-bezier(.5,0,.2,1);
}
.ring:hover .rrun, .ring:focus-visible .rrun {
  opacity: 1;
  stroke-dashoffset: calc(var(--seg) - var(--half));
}
/* The two arcs and the tick that rotated round the outside are gone: the SVG
   above draws the arcs, and the travelling segment replaced the tick. */
@keyframes spin { to { transform: rotate(360deg); } }
.ring span { font-family: var(--body); font-weight: 700; font-size: 10px; letter-spacing: .16em;
             text-transform: uppercase; text-align: center; line-height: 1.5; max-width: 62%; }


/* ------------------------------------------------- chrome, coloured per item */
/* Each fixed item resolves its own colour from what is directly behind it and
   flips as it crosses, one at a time. A full-height gradient scrim was the
   previous answer and it read as a smear that appeared and vanished. */
.chrome { position: fixed; inset: 0; pointer-events: none; z-index: 45; }
.chrome a { pointer-events: auto; }
[data-ci] { color: var(--chrome-ink); transition: color .40s ease, text-shadow .40s ease; }
[data-ci][data-on="l"] { color: #fff; }
/* an actual dark colour, NOT var(--ink): on a dark-mode site the ink is light,
   so using it here left both options light and the item invisible either way */
[data-ci][data-on="d"] { color: #16171b; }
/* Where the background is genuinely mid-tone, no colour reaches 4.5:1 for 10px
   text. A tight halo in the opposite tone is added per item, only when that
   item's own contrast would fail, so nothing appears anywhere else. */
[data-ci][data-halo="l"] { text-shadow: 0 0 5px rgba(0,0,0,.92), 0 0 12px rgba(0,0,0,.7); }
[data-ci][data-halo="d"] { text-shadow: 0 0 5px rgba(255,255,255,.95), 0 0 12px rgba(255,255,255,.75); }

/* The fixed call block passes over live copy for the whole length of the page,
   and contrast against the BACKGROUND does not save it there: a white phone
   number over a white review is legible against the photograph behind both and
   still unreadable, because the two runs of text merge. Every chrome item
   therefore carries a permanent, quiet separation halo in its opposite tone.
   Kept well below the emergency halo above so it reads as depth, not as an
   outline. */
[data-chrome] [data-on="l"] { text-shadow: 0 0 9px rgba(6,7,9,.62), 0 1px 2px rgba(6,7,9,.5); }
[data-chrome] [data-on="d"] { text-shadow: 0 0 9px rgba(255,255,255,.68), 0 1px 2px rgba(255,255,255,.55); }
[data-chrome] [data-halo] { text-shadow: none; }   /* the emergency halo wins */

.mark { position: absolute; left: clamp(18px, 2.4vw, 38px); top: clamp(18px, 2.2vw, 34px);
        width: clamp(56px, 5vw, 74px); aspect-ratio: 1; }
.mark svg { width: 100%; height: 100%; overflow: visible; }
/* constant base rotation, so the ring never stops even when nothing is
   happening; the scroll-driven offset rides on the parent group */
.mark .ringtext { animation: spin 34s linear infinite; }
/* float animates `translate` and `rotate`, never `transform`: an animation
   overrides an inline style, so a float on `transform` silently cancelled the
   parallax the script writes to the same element. */
@keyframes float {
  from { translate: 0 0; rotate: 0deg; }
  to   { translate: 0 -16px; rotate: 2.2deg; }
}
@media (prefers-reduced-motion: reduce) {
  .mark .ringtext, .cut, .ring::after { animation: none; }
}
.nav { position: absolute; right: clamp(18px, 2.4vw, 38px); top: clamp(18px, 2.2vw, 34px); text-align: right; }
.nav .big { font-family: var(--display); font-size: clamp(15px, 1.5vw, 21px); line-height: 1.06;
            text-transform: uppercase; text-decoration: underline; text-underline-offset: 4px; display: block; }
.nav .small { font-family: var(--body); font-weight: 700; font-size: 10px; letter-spacing: .18em;
              text-transform: uppercase; display: block; margin-top: 9px; text-decoration: none; }
/* Every hover on the site eases. An opacity change with no transition is a
   state flip, and next to animations that all run for a second it reads as a
   different, cheaper website. */
.nav .small, .nav .big, .garrow, .mrow, .sopt, .pill {
  transition: opacity .55s cubic-bezier(.3,0,.2,1),
              color .55s cubic-bezier(.3,0,.2,1),
              background .55s cubic-bezier(.3,0,.2,1);
}
.nav .small:hover { opacity: .65; }

/* ------------------------------------------------------ progress, one track */
/* ONE track split by the number, and the number TRAVELS down it. Above it the
   travelled segment, below it the remaining segment, with a transparent gap so
   the rule never slices the digits. */
.rail { position: fixed; left: clamp(18px, 2.4vw, 38px); top: 0; height: 100vh;
        display: flex; flex-direction: column; align-items: center; justify-content: center;
        gap: 26px; z-index: 40; pointer-events: none; }
.ptrack { position: relative; width: 1px; height: 46vh; }
.pdone, .pleft { position: absolute; left: 0; width: 1px; background: currentColor; }
.pdone { top: 0; }
.pleft { opacity: .30; }
.pnum { position: absolute; top: 0; left: 50%; margin-left: -1.1em; margin-top: -7px;
        width: 2.2em; text-align: center;
        font-family: var(--body); font-weight: 700; font-size: 10px; letter-spacing: .1em;
        font-variant-numeric: tabular-nums; }
.pscroll { writing-mode: vertical-rl; font-family: var(--body); font-weight: 700; font-size: 9px;
           letter-spacing: .3em; text-transform: uppercase; opacity: .75; }

/* --------------------------------------------------------------- sections --- */
section { position: relative; }
/* Every section fills the screen. Seeing parts of three sections at once is the
   single clearest tell of a slideshow, and a short section (the review row was
   the worst offender) reads as a gap rather than a place. */
/* svh, not vh: on a browser with dynamic toolbars vh is the LARGEST viewport
   and a "full screen" section is then shorter than the screen actually is.
   min() of the two keeps it exactly one screen at any zoom or chrome state. */
.pad { --padx: clamp(22px, 9vw, 170px); --padr: var(--padx);
       padding: clamp(122px, 17vh, 200px) var(--padr) clamp(122px, 17vh, 200px) var(--padx);
       min-height: 100vh; min-height: 100svh;
       display: grid; align-content: center; }
@media (max-width: 760px) {
  /* Set the same variables the desktop rule uses, or a picture bleeding by
     --padx off a 22px padding overflows the page by the difference. */
  .pad { --padx: 22px; --padr: 22px;
         padding: clamp(96px, 13vh, 140px) 22px; min-height: 100svh; }
}
.hold { max-width: 1560px; margin: 0 auto; width: 100%; }

/* Sections change with a SHAPE, never a seam.

   The device, corrected. The shape is a SEMICIRCLE, flat side down, as wide as
   the page, and it never changes size. It rises into view until its flat edge
   meets the next section, whose ground is the same colour, so the two become one
   surface. Because the fill is a flat colour and not a photograph, the handoff
   is invisible; this transition therefore only ever runs between a section with
   a solid ground and another with a solid ground.

   The expansion is an illusion and it comes entirely from the WORDS ON THE RIM
   spreading apart as it rises. Nothing about the geometry grows. Three earlier
   attempts scaled the shape itself, which is exactly what the effect is not. */
/* NOT clipped. The semicircle rises OVER the section above it, progressively
   covering that section's content, which is the whole point: its flat edge is
   the top edge of the next section, and the shape sweeps up out of it. Clipped
   inside its own band it could only ever rise into an empty screen. */
.riser { position: relative; overflow: visible; z-index: 3; }
.riser > * { position: relative; z-index: 1; }

/* The semicircle. Its diameter is the page width, so the rim leaves the frame
   exactly at the left and right edges. preserveAspectRatio is off, so the shape
   stretches with the viewport and the rim always meets both edges. */
/* Height is exactly half the width, so the shape is a true semicircle whose
   diameter is the page: its rim leaves the frame precisely at the left and
   right edges. Capping the height with a vh value broke that and produced a
   shallow dome instead. */
/* The shape does not move on its own timeline AT ALL. Its flat edge is welded
   to the top of the next section and it travels up the page at exactly the speed
   that section does, because it is simply sitting on it. Giving it a
   scroll-driven translate made it drift against the thing it is attached to,
   which is why the join kept opening and closing.

   The only thing on a timeline is the word spacing on the rim. That is the
   entire effect. */
.arclabel {
  position: absolute; left: 0; bottom: 0; width: 100%;
  /* Exactly half the width. Capping the height against the viewport turned it
     into a squashed ellipse on any screen wider than it is tall, which is not
     the shape. It is a semicircle or it is nothing. */
  height: 50vw;
  z-index: 2; pointer-events: none; overflow: visible;
}
.riser::before {
  content: ""; position: absolute; left: 0; bottom: 0; width: 100%;
  height: 50vw; z-index: 1;
  background: var(--riser, var(--deep));
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
}

/* The rim lettering. It sits ON the shape, so it takes the fill's contrasting
   ink. The words open outward along the arc as the shape rises, and the phrase
   is short enough that every word is on the rim at maximum spread. */
.arclabel text {
  /* the display face, as everywhere else a large word appears */
  font-family: var(--display); font-weight: var(--display-weight, 400);
  font-size: 58px;
  letter-spacing: calc(1.4px + var(--sp, 0) * 3px);
  /* Spreads much further, and cannot run off the arc: the rim is about 1450
     user units long and the phrase is around 500, so 2 gaps of 210 still leave
     room at full spread. */
  word-spacing: calc(8px + var(--sp, 0) * 210px);
  text-transform: uppercase; fill: var(--riserink, currentColor);
  /* the lettering has its own opening: it fades up as the shape arrives */
  opacity: clamp(0, calc((var(--sp, 0) - 0.06) * 4), 1);
}
@media (max-width: 760px) {
  .arclabel text { font-size: 30px; word-spacing: calc(5px + var(--sp, 0) * 110px); }
}
@media (prefers-reduced-motion: reduce) {
  .arclabel, .riser::before { translate: none; }
}

/* the transition band carries the shape and nothing else. Tall enough that the
   semicircle is read as a shape arriving, no taller. */
/* The band has NO HEIGHT and NO BACKGROUND. It is a marker for where the next
   section starts, and the shape hangs upward from it over the section above.
   Given a height of its own it became an exposed strip of flat colour between
   the photograph and the shape: a section that is not a section, which is
   exactly what it looked like. */
.tband { height: 0; background: none; }

/* ------------------------------------------------------- horizontal scroll --- */
/* The Chinese-scroll device: one long artwork panned across, not a sequence of
   slides. Vertical scroll becomes horizontal travel while the section is pinned.
   This is the variety of movement that stops a page reading as a vertical stack. */
[data-hscroll] { position: relative; }
.hsticky { position: sticky; top: 0; height: 100vh; overflow: hidden;
           display: flex; align-items: center; }
/* A track of short panels floating in the middle of a tall dark section reads as
   a strip in a void. The panels carry real height so the pinned screen is
   composed rather than mostly empty. */
.hsticky .frame { height: min(62vh, 620px); }
.hsticky .frame.v { height: min(74vh, 720px); aspect-ratio: auto; }
.htrack { display: flex; align-items: center; gap: clamp(26px, 3.4vw, 70px);
          padding-left: clamp(22px, 9vw, 170px);
          /* a full screen of run-out, so the last panel is never jammed against
             the right edge and never truncated by the sticky container */
          padding-right: clamp(180px, 40vw, 620px);
          will-change: transform; }
.hpanel { flex: 0 0 auto; max-width: 78vw; }
.hbig { padding-right: .08em; }        /* room for the final letter's overhang */
.hpanel { flex: 0 0 auto; }
.hbig { font-family: var(--display); font-weight: var(--display-weight,400);
        font-size: clamp(64px, 15vw, 230px); line-height: .82; letter-spacing: -.03em;
        text-transform: uppercase; white-space: nowrap; }
@media (max-width: 760px) {
  /* on a phone a pinned horizontal track fights the thumb; let it stack */
  [data-hscroll] { height: auto !important; }
  .hsticky { position: static; height: auto; }
  .htrack { flex-direction: column; align-items: flex-start; transform: none !important; gap: 40px; }
  /* a single long town name at 13vw still exceeds a 390px viewport, which is
     the classic sideways-scroll break; let it wrap and never exceed its box */
  .hbig { white-space: normal; font-size: clamp(34px, 11vw, 74px);
          overflow-wrap: anywhere; max-width: 100%; }
  .htrack { max-width: 100%; }
  .hpanel { max-width: 100%; }
}

/* ------------------------------------------------------------- menu rows --- */
.menu { position: relative; padding-left: 20px; }
.mbar { position: absolute; left: 0; top: 0; width: 2px; background: currentColor;
        transition: transform .5s cubic-bezier(.16,1,.3,1), height .5s cubic-bezier(.16,1,.3,1); }
.mrow {
  display: block; background: none; border: 0; padding: 0 0 6px; margin: 0 0 6px;
  font-family: var(--display); font-size: clamp(19px, 2.2vw, 34px); line-height: 1.08;
  text-transform: uppercase; color: inherit; text-align: left; cursor: pointer;
  opacity: .4; transition: opacity .32s ease;          /* measured: 0.40 against 1.00 */
}
.mrow.on { opacity: 1; }
.mrow:hover, .mrow:focus-visible { opacity: 1; outline: none; }  /* hover ADDS, never steals the selection */

/* ------------------------------------------------------------- utilities --- */
.cut { position: absolute; pointer-events: none; z-index: 3; }   /* foreground occlusion */
.stat { font-family: var(--display); font-size: clamp(38px, 5.4vw, 92px); line-height: .9;
        font-variant-numeric: tabular-nums; }
.hr { height: 1px; background: currentColor; opacity: .22; border: 0; margin: 0; }
.quote { font-family: var(--display); font-size: clamp(17px, 1.55vw, 23px);
         line-height: 1.4; max-width: 60ch; }
.tel { font-family: var(--display); font-size: clamp(34px, 7vw, 118px); line-height: .88;
       text-decoration: none; display: inline-block; }
.pill { display: inline-flex; align-items: center; gap: .7em; border: 1px solid currentColor;
        border-radius: 999px; padding: .85em 1.6em; font-family: var(--body); font-weight: 700;
        font-size: 11px; letter-spacing: .16em; text-transform: uppercase; text-decoration: none;
        transition: background .3s ease, color .3s ease; }
.pill:hover { background: currentColor; }
.pill:hover span { mix-blend-mode: difference; }

/* the demo disclosure: required, persistent, and out of the reading column */
.disclose {
  position: fixed; right: 14px; bottom: 12px; z-index: 90;
  background: rgba(16,16,20,.92); color: #fff; font-family: var(--body); font-size: 10px;
  letter-spacing: .03em; padding: .55em 1em; border-radius: 999px;
  display: flex; gap: .5em; align-items: center; max-width: min(46ch, calc(100vw - 28px));
  backdrop-filter: blur(6px);
}
@media (max-width: 760px) { .disclose { right: 10px; left: 10px; max-width: none; justify-content: center; } }

:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }


/* ------------------------------------------------------- staggered entrance --
   Nothing on the page is static. Plain components (an hours table, a list of
   facts, the ask slots) get an entrance too, staggered down the list, so the
   section assembles rather than simply being there. */
[data-stagger] > * { opacity: 0; transform: translateY(26px);
  transition: opacity .9s ease, transform 1.05s cubic-bezier(.16,1,.3,1);
  transition-delay: calc(var(--i, 0) * 95ms); }
[data-stagger].in > * { opacity: 1; transform: none; }
.hrs tr { opacity: 0; transform: translateY(20px);
  transition: opacity .8s ease, transform 1s cubic-bezier(.16,1,.3,1), color .25s ease;
  transition-delay: calc(var(--i, 0) * 80ms); }
.hrs.in tr { opacity: .55; transform: none; }
.hrs.in tr:hover { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  [data-stagger] > *, .hrs tr { opacity: 1; transform: none; transition: none; }
}

/* --------------------------------------------------------------- logo mark --
   A small monocolour mark closes every large block of text, the way a printed
   article takes an end mark. It is drawn, not an image, so it inherits colour
   from whatever ground it lands on. */
/* .mark is already taken by the fixed corner logo. Naming this one .mark too
   overwrote that element's positioning and left it as a blob in the top-left
   corner of every page. */
.endmark { display: block; width: 24px; height: 24px; margin-top: clamp(22px, 3vh, 40px);
           opacity: 0; transform: translateY(14px) rotate(-25deg);
           transition: opacity .9s ease .35s, transform 1.1s cubic-bezier(.16,1,.3,1) .35s; }
.endmark.in { opacity: .55; transform: none; }
@media (prefers-reduced-motion: reduce) { .endmark { opacity: .55; transform: none; } }




/* --------------------------------------------------------- full-page gallery --
   Composed off the reference, not off a grid. The picture is the whole screen;
   the menu sits at the TOP RIGHT with a thin rule to its left; the statement
   sits at the BOTTOM LEFT and runs about half the screen wide. Nothing occupies
   the middle and there is no dividing line, because two equal boxes either side
   of an invisible centre is exactly the symmetry that made this section boring.
   There are no arrows, no index and no timer: the menu is the control. */
/* isolate: without its own stacking context the gallery's z-index 4 layers
   compete with the transition band's z-index 3 in the PAGE's context, so the
   statement and the menu drew straight through the semicircle rising over
   them. */
/* Taller than the screen by exactly the height of the semicircle that follows
   it, with its own contents PINNED to the first screenful. The shape is welded
   to the section's end, so with a 100vh gallery it sat permanently across the
   bottom three quarters of the picture. Given this extra run, the gallery holds
   still while the shape rises up into it from below, which is the device. */
/* One screen to read, then a long HOLD, then the run the shape needs to rise
   through. At 70vh of hold a single flick brought the dome up over the picture
   while the reader was still looking at it; a screen and a half means the shape
   cannot appear until they have deliberately scrolled past.
   The shape's height is capped: half the page width is 1720px on an ultrawide,
   taller than the screen, and a dome whose top never appears is not a dome. */
.fullgal { position: relative;
           --dome: 50vw;
           min-height: calc(250vh + var(--dome));
           min-height: calc(250svh + var(--dome));
           isolation: isolate; z-index: 0;
           background: #090a0c; color: #f4f2ee; }
@media (max-width: 760px) { .fullgal { min-height: 100svh; } }
.fullgal > .fg { position: sticky; top: 0; height: 100vh; height: 100svh;
                 overflow: hidden; }
.fullgal .shots { position: absolute; inset: 0; }

/* The picture is exactly the viewport, covered and centred. The shared frame
   rule offsets its image by -25% and sizes it 150% tall for the ambient drift,
   which in a full-screen frame pushes the visible centre a long way up the
   picture: that is why these zoomed into an apparently random part of the
   image, and why a strip of ground showed at the bottom. */
.fullgal .frame > img {
  top: -6%; left: -6%; width: 112%; max-width: none; height: 112%;
  object-fit: cover; object-position: 50% 50%;
}
/* The leaving picture's origin is on the side it is travelling TOWARD, so the
   zoom pulls it that way; the arriving picture's origin is on the side it is
   coming FROM, so its zoom-out settles inward. */
.shot.wiping > .frame > img { transform-origin: 78% 50%; }
.shot.under  > .frame > img { transform-origin: 22% 50%; }

/* Weighted where the type is and nowhere else: heavy in the bottom left under
   the statement, a soft pool behind the menu at the top right, and a flat floor
   everywhere so a pale patch of sky cannot beat it. The middle of the picture is
   left alone, which is the point of putting nothing there. */
.fgscrim { position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background:
    radial-gradient(78% 62% at 12% 96%, rgba(6,7,9,.90) 0%, rgba(6,7,9,.52) 46%,
                                        rgba(6,7,9,0) 100%),
    radial-gradient(52% 44% at 88% 12%, rgba(6,7,9,.82) 0%, rgba(6,7,9,0) 100%),
    linear-gradient(to top, rgba(6,7,9,.66) 0%, rgba(6,7,9,.16) 42%,
                            rgba(6,7,9,.06) 66%, rgba(6,7,9,.34) 100%),
    rgba(6,7,9,.30); }

/* top right */
/* Measured off era-residence at 1440x900: the menu column starts at 69.5% of
   the viewport and its rows are LEFT aligned from there, ragged right, with the
   rule on their left. Right-aligning them against the far edge is what made
   this corner look like a caption instead of a column. */
.fgmenu { position: absolute; z-index: 4; left: 69.5%; right: clamp(22px, 3vw, 60px);
          top: 16%; }
.fullgal .menu { padding-left: 18px; text-align: left; }
.fullgal .mrow { font-size: clamp(18px, 2.4vw, 36px); line-height: 1.12;
                 text-align: left; width: 100%; }

/* bottom left */
/* And the statement: left edge at 11.9%, running to about 49%, sitting low.
   The label and the FIRST line hang further in, at 21.6%, which is the detail
   that stops the block reading as a rectangle. */
.fgsay { position: absolute; z-index: 4; left: 11.9%;
         bottom: 21%; width: 37%;
         display: grid; gap: clamp(8px, 1.1vh, 16px); }
/* One left edge for everything in this block. The hanging indent came off the
   reference, where the label and the first line start further in than the rest,
   and it reads as a mistake rather than as typesetting at this size. */
.fgsay, .fgsay > .label, .fullgal .cap, .fullgal .cap .lede,
.fullgal .cap .mini, .fullgal .cap .prov {
  text-align: left; text-indent: 0; margin-left: 0;
}
.fgcaps { position: relative; min-height: 5.2em; }
.fullgal .cap .lede { font-family: var(--display); text-transform: uppercase;
                      font-size: clamp(19px, 2.5vw, 40px); line-height: 1.08;
                      letter-spacing: -.005em; }
/* The reference's statement is a label and one sentence, nothing else. Ours
   also carried the customer quote and its provenance, which overflowed the
   block, fell below the fold and was clipped by the section: text that is on
   the page and can never be read. The quote belongs in the reviews section,
   where it already is. */
.fullgal .cap .mini, .fullgal .cap .prov { display: none; }
.fgring { position: absolute; z-index: 4; left: 69.5%; right: auto;
          bottom: 9%; }
@media (max-width: 900px) {
  .fgmenu { position: static; max-width: none; padding: 22vh 22px 0; }
  .fullgal .menu, .fullgal .mrow { text-align: left; }
  .fgsay { position: static; width: auto; padding: 24px 22px 12vh; }
  .fgring { display: none; }
  .fullgal > .fg { position: relative; }
  .fullgal .shots { position: absolute; }
}

/* ------------------------------------------------------- closing photograph --
   The last section used to be flat colour directly after another flat colour.
   A photograph behind the phone number keeps the alternation and ends the page
   somewhere rather than on a swatch. */
.endbg { position: absolute; inset: 0; overflow: hidden; z-index: 0; }
.endbg img { position: absolute; top: -18%; left: -3%; width: 106%; height: 136%;
             object-fit: cover; filter: brightness(.34) saturate(.72); }
.end::after { content: ""; position: absolute; inset: 0; z-index: 1;
  pointer-events: none;
  background: radial-gradient(120% 78% at 50% 52%, rgba(9,10,12,.82) 0%,
                              rgba(9,10,12,.52) 58%, rgba(9,10,12,.30) 100%); }
.end > .hold { position: relative; z-index: 2; }


/* ------------------------------------------------------------ magazine spread --
   Four movements on one pinned timeline. The phases are named so the CSS reads
   as the storyboard does.

   The frame is always the full screen and a CLIP is what moves. That is what
   keeps the title from reflowing as the scene opens, and it is what lets all
   four edges arrive at the screen together instead of the box finishing one
   axis and then shuffling the other two out to meet it. */
.mag { position: relative; }
.magsticky {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  display: block; background: #08090b; color: #f4f2ee;
  /* on the PINNED timeline: nothing moves until the scene has landed */
  /* On a 480vh section the pin is 380vh, so each phase below is measured in
     hundreds of pixels of scroll rather than tens. The expansion used to get
     18% of a 220vh pin, which is 356px: at glide speed that is four frames, and
     four frames of a size change reads as a jump, not an animation. */
  --p1: clamp(0, calc((var(--pp, 0) - .03) / .26), 1);   /* halves close vertically */
  --p2: clamp(0, calc((var(--pp, 0) - .31) / .17), 1);   /* and then merge across  */
  --p3: clamp(0, calc((var(--pp, 0) - .50) / .30), 1);   /* the frame opens out    */
  --p4: clamp(0, calc((var(--pp, 0) - .82) / .18), 1);   /* travel into the scene  */
  /* one rectangle, shared by the picture and by the clip over the type */
  --bw: calc(31vw + var(--p3) * 69vw);
  --bh: calc(40vh + var(--p3) * 60vh);
  --ix: calc((100vw - var(--bw)) / 2);
  --iy: calc((100vh - var(--bh)) / 2);
}
@media (max-width: 760px) {
  .magsticky { height: 100svh; --bw: calc(72vw + var(--p3) * 28vw); }
}

.magbox {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  width: var(--bw); height: var(--bh); overflow: hidden;
}

/* Two halves of ONE photograph. They start close together and STAGGERED, one
   high and one low, and close the vertical gap first; only when they are level
   do they slide the short distance across into a single frame. The earlier
   version parted them by half the frame and slid them together like a pair of
   doors, which is a different and much cheaper effect. */
.maghalf { position: absolute; inset: 0; overflow: hidden; z-index: 1; }
/* The vertical offsets must be OPPOSITE. Written with the same sign, both
   halves simply sat lower and there was no stagger at all: the pair read as one
   picture that had slipped down the frame. */
/* The halves OVERLAP by a fraction of a percent. Clipped at exactly 50% each,
   sub-pixel rounding leaves a hairline of background showing down the join once
   they have merged, and a seam is the one thing a merge cannot have. */
.maghalf.l {
  clip-path: inset(0 49.85% 0 0);
  transform: translate(calc((var(--p2) - 1) * 5%), calc((1 - var(--p1)) * 15%));
}
.maghalf.r {
  clip-path: inset(0 0 0 49.85%);
  transform: translate(calc((1 - var(--p2)) * 5%), calc((var(--p1) - 1) * 15%));
}

/* The picture is 2.35x the frame and travels 57.5% of its own height, which puts
   its bottom edge exactly on the frame's bottom edge at the end of the phase.
   Both halves pan identically or the seam reopens. */
/* Once the halves have landed, a single UNDIVIDED copy of the photograph fades
   in over them. Two clipped layers meeting at 50% cannot be relied on to line up
   to the pixel: they are separately composited, their clip edges round to
   different sub-pixels, and a thin horizontal line crossing the join renders as
   two offset segments. After the merge there is no join at all. */
.magwhole { position: absolute; inset: 0; z-index: 2; overflow: hidden;
            opacity: clamp(0, calc((var(--p2) - .82) * 6), 1); }
.magpan { position: absolute; inset: 0; }
.magpan img { position: absolute; left: 0; top: 0; width: 100%; height: 235%;
  object-fit: cover; object-position: 50% 12%;
  filter: brightness(.62) saturate(.9) contrast(1.04);
  transform: translateY(calc(var(--p4) * -57.5%)); }

/* The type is laid out ONCE, at the size it will finish at, across the whole
   screen, and is then cropped by exactly the rectangle the frame occupies. It
   never rewraps, never moves, and the parts of it outside the frame simply are
   not drawn yet. */
/* A cover puts its masthead at the TOP and lets the picture run long underneath
   it. Centred in the frame it read as a caption laid over a photograph. */
.magtype {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  display: grid; align-content: start; justify-items: center;
  gap: clamp(8px, 1.2vh, 16px); text-align: center;
  /* Placed against the WINDOW's edge, not the section's top. The window opens at
     40vh centred, so its top edge is at 30vh, and the title starts 0.45 of its
     own height above that: the same slice of it is cropped at every screen
     width. Measured from the section instead, a wide screen fitted the name onto
     one line and left the whole thing above the window, so all you could see was
     the town underneath it. */
  padding: 0 clamp(16px, 4vw, 60px);
  clip-path: inset(var(--iy) var(--ix) var(--iy) var(--ix));
}

/* A Vogue cover sets its masthead in capitals, and heavy. Title case at a light
   weight is a different magazine entirely. */
.magtitle {
  font-family: var(--display); font-weight: 700; line-height: .9;

  font-size: clamp(40px, 10.5vw, 176px); letter-spacing: -.015em;
  text-transform: uppercase; text-wrap: balance; margin: 0;
}
.magkick, .magsub { opacity: calc(.35 + var(--p3) * .65); }
@media (prefers-reduced-motion: reduce) {
  .maghalf.l, .maghalf.r { transform: none; }
  .magsticky { --p3: 1; }
  .magpan img { transform: none; }
}

/* --------------------------------------------------------------- open scene --
   The same idea in one movement: a frame that opens to full bleed while the
   picture inside it moves more slowly, so the frame outruns its contents and
   the viewer is drawn in. The giant word is cropped by the frame's own edge and
   RESOLVES as it opens: a deliberate crop, which is a crop you can see the
   cause of and which finishes. */
.scene-open { position: relative; height: 240vh; }
.scene-open .clipper {
  position: sticky; top: 0; height: 100vh; overflow: hidden;
  background: #08090b; color: #f6f4f0;
  --op: clamp(0, calc((var(--pp, 0) - .06) / .60), 1);
  --bw: calc(44vw + var(--op) * 56vw);
  --bh: calc(46vh + var(--op) * 54vh);
  --ix: calc((100vw - var(--bw)) / 2);
  --iy: calc((100vh - var(--bh)) / 2);
}
.scene-open .clipper > img {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  width: 100vw; height: 100vh; object-fit: cover;
  filter: brightness(.66) saturate(.92);
  clip-path: inset(var(--iy) var(--ix) var(--iy) var(--ix));
  scale: calc(1.16 - var(--op) * 0.16);      /* the picture lags the frame */
}
.scene-open .bigword, .scene-open .undercap {
  position: absolute; left: 0; width: 100%; text-align: center;
  clip-path: inset(var(--iy) var(--ix) var(--iy) var(--ix));
  z-index: 2; margin: 0;
}
.scene-open .bigword {
  top: 50%; translate: 0 -58%;
  font-family: var(--display); font-weight: 700; text-transform: uppercase;
  font-size: clamp(48px, 15vw, 250px); line-height: .86; letter-spacing: -.02em;
}
.scene-open .undercap { top: 50%; translate: 0 190%; }

/* Type over a photograph casts a SHADOW. The previous build put a translucent
   gradient rectangle behind it, which has hard edges wherever the gradient
   stops and reads as a rendering fault rather than as a design decision. A
   shadow scaled to the type does the same job and has no edges at all. */
/* Body copy over a photograph may carry a shadow. DISPLAY TYPE MAY NOT: at
   that size a shadow reads as a printing fault, and it is the thing Aaron has
   now asked to have removed twice. The scrim under these titles is what makes
   them legible, not a glow around each letter. */
.onphoto {
  text-shadow: 0 1px 2px rgba(6, 8, 11, .55),
               0 0.03em 0.10em rgba(6, 8, 11, .5),
               0 0.08em 0.28em rgba(6, 8, 11, .38);
}
.onphoto[data-on="d"] {
  text-shadow: 0 1px 2px rgba(255, 255, 255, .6),
               0 0.03em 0.10em rgba(255, 255, 255, .55),
               0 0.08em 0.28em rgba(255, 255, 255, .4);
}
.magtitle, .mgtitle, .bigword, .hbig, .mast,
.magtitle .c, .mgtitle .c, .bigword .c, .hbig .c, .mast .c {
  text-shadow: none !important;
}


/* --------------------------------------------------- keep clear of the chrome --
   The call block is fixed to the top right, so any section whose content reaches
   the top of the screen runs underneath it: a review column had the phone number
   printed straight through the word "Art". Reserve a gutter wide enough for the
   chrome plus its own margin. Tested by tools/chrome-clash.mjs. */
@media (min-width: 900px) {
  /* set the VARIABLE, not the padding, so a picture that bleeds off this edge
     still knows how far it has to travel to reach it */
  .pad { --padr: max(clamp(22px, 9vw, 170px), 215px); }
  .fgbody, .magsticky { padding-right: max(clamp(22px, 9vw, 170px), 215px); }
  .hsticky { padding-right: 215px; }
}


/* ------------------------------------------------------- staggered bleed pair --
   The reference never floats a picture in the middle of an empty column with
   text beside it. Pictures are LARGE, pushed hard into an edge so they bleed off
   the page, with their text directly underneath; the second pair does the same
   against the opposite edge but lower, so the two read as staggered rather than
   as a row. Something touches every edge of the screen. */
.stagger2 { display: grid; grid-template-columns: 1fr; gap: clamp(30px, 4vw, 60px); }
@media (min-width: 900px) {
  .stagger2 { grid-template-columns: 1.04fr .96fr; gap: clamp(18px, 2vw, 40px);
              align-items: start; }
  .stagger2 .low { margin-top: clamp(64px, 11vh, 130px); }   /* the stagger */
}
/* The caption belongs TO the picture above it, so it sits close under it. Two
   separate spacings stacked (the column gap plus the label's own margin) pushed
   it far enough away to read as an unrelated block, worst on the column that
   also carries the two-state switch. */
.stcol { display: grid; gap: clamp(8px, 1vh, 14px); align-content: start; }
.stcol .label { margin-top: clamp(2px, .5vh, 8px); }
.stcol .bleed { margin-bottom: clamp(2px, .6vh, 10px); }
/* bleeding off the page edge, using the section's own padding, so it works at
   every viewport width without a hand-written negative margin */
.bleed { position: relative; }
.bleed.l { margin-left: calc(-1 * var(--padx, 0px)); }
.bleed.r { margin-right: calc(-1 * var(--padr, 0px)); }
@media (max-width: 899px) {
  .bleed.l { margin-right: calc(-1 * var(--padr, 0px)); }
  .bleed.r { margin-left: calc(-1 * var(--padx, 0px)); }
}
/* Large: the picture IS the section, not an illustration beside it.
   The aspect ratio has to go. Holding a ratio while capping the height caps the
   WIDTH too, so the picture stopped short of the edge it was supposed to bleed
   off and the whole point of the layout was lost. Height is set directly and
   width simply fills the column plus the bleed. */
/* Big. Reaching the edge is only half of it: a picture that touches the border
   and is then only 396px tall in a 900px screen still leaves the middle of the
   page empty, which is the thing this layout exists to prevent. */
.stagger2 .bleed .frame { aspect-ratio: auto; width: 100%; height: clamp(320px, 62vh, 700px); }
.stagger2 .bleed .panes, .stagger2 .bleed .pane { width: 100%; }
.stagger2 .bleed .pane .frame { height: clamp(320px, 62vh, 700px); }


/* ---------------------------------------------------------------- entrance --
   A loading bar, then an arch you travel through.

   The "hole" is the element itself; everything around it is one enormous box
   shadow in the ground colour. That is what makes this an OPENING rather than a
   drawn outline: there is nothing to draw, there is only what the ground is not
   covering. Growing the hole past the frame reads as the viewer moving forward
   through the arch and arriving inside the picture behind it.

   The previous version put a hairline arch OUTLINE at the foot of the page and
   revealed it from the bottom, which is neither the shape, the place, nor the
   idea. */
html.entering { overflow: hidden; }
.enter {
  position: fixed; inset: 0; z-index: 9999; pointer-events: none;
  color: var(--enterink, #fff);
}
.enter::before {
  content: ""; position: absolute; left: 50%;
  top: var(--at, 63%);
  width: var(--aw, 13vmin); height: var(--ah, 21vmin); translate: -50% -50%;
  border-radius: 50% 50% 0 0 / 62% 62% 0 0;      /* round top, straight sides */
  box-shadow: 0 0 0 150vmax var(--enterbg, #101014);
  transition: width 1.05s cubic-bezier(.5,0,.18,1),
              height 1.05s cubic-bezier(.5,0,.18,1),
              top 1.05s cubic-bezier(.5,0,.18,1);
}
.enterbar {
  position: absolute; left: 50%; bottom: 13vh; translate: -50% 0;
  width: min(240px, 42vw); height: 2px; overflow: hidden;
  background: color-mix(in srgb, currentColor 24%, transparent);
  transition: opacity .45s ease;
}
.enterbar i {
  position: absolute; inset: 0; display: block; background: currentColor;
  transform-origin: 0 50%; transform: scaleX(var(--load, 0));
  transition: transform .35s cubic-bezier(.3,0,.2,1);
}
.entername {
  position: absolute; left: 50%; bottom: calc(13vh + 26px); translate: -50% 0;
  margin: 0; font-family: var(--body); font-weight: 700; font-size: 10px;
  letter-spacing: .3em; text-transform: uppercase; opacity: .72;
  transition: opacity .45s ease;
}
/* The bar and the wordmark clear FIRST, on their own, and are gone before the
   arch moves at all. Fading them during the opening left the loading furniture
   on screen while the site behind it was being revealed. */
.enter.hushed .enterbar, .enter.hushed .entername { opacity: 0; }
/* The second opening starts from wherever the first one stopped, so the two
   read as one continuous move forward rather than a jump. */
.enter.go::before { animation: archThrough 1.6s cubic-bezier(.62,0,.22,1) forwards; }
@keyframes archThrough {
  0%   { width: var(--aw); height: var(--ah); top: var(--at); }
  100% { width: 340vmax;   height: 380vmax;  top: 50%; }
}
@media (prefers-reduced-motion: reduce) { .enter { display: none; } }

/* Centred children: text-align cannot centre a fixed-width block, which left
   the call ring and the end mark stranded against the left edge. */
.end .ring, .end .endmark { margin-inline: auto; }


/* --------------------------------------------------- readability over photos --
   Choosing the better of black and white per item is necessary and not
   sufficient: over a photograph there are places where neither clears, and the
   type sinks into the picture. Every run of text inside a photographic section
   therefore also casts a shadow, scaled to its own size, in the opposite tone.
   A shadow has no edges, so unlike the gradient panels it replaced it cannot
   read as a rendering fault. */
.hero, .fullgal, .mag, .scene-open, .end, .hsec { --shade: rgba(6, 8, 11, .58); }
.hero [data-on="d"], .fullgal [data-on="d"], .mag [data-on="d"],
.scene-open [data-on="d"], .end [data-on="d"], .hsec [data-on="d"] {
  --shade: rgba(255, 255, 255, .62);
}
.hero :is(p,a,b,td,th,figcaption,button),
.fullgal :is(p,a,b,button),
.mag :is(p,span,a,b),
.scene-open :is(p,a,b),
.end :is(p,a,b),
.hsec :is(p,a,b) {
  text-shadow: 0 1px 2px var(--shade), 0 .04em .14em var(--shade);
}
/* the rim lettering is on a flat fill and needs none of this */
.arclabel text, .enter * { text-shadow: none; }


/* the hours section: a large picture hard against the left edge, then the two
   columns of facts. Nothing floats in the middle of an empty screen. */
.visit3 { display: grid; grid-template-columns: 1fr; gap: clamp(28px, 4vw, 64px); }
@media (min-width: 980px) {
  .visit3 { grid-template-columns: minmax(0, .86fr) minmax(0, 1fr) minmax(0, 1fr);
            gap: clamp(30px, 4vw, 72px); align-items: center; }
}
.visit3 .vpic .frame { aspect-ratio: auto; width: 100%; height: clamp(320px, 74vh, 760px); }
@media (max-width: 979px) { .visit3 .vpic { display: none; } }


/* -------------------------------------------------------------- line lists --
   Hours, and the facts we are missing. A list that appears all at once is not
   an animation, it is a state change. Each row arrives in turn, and inside a
   row the key arrives, then the value, while the rule under them draws itself
   from left to right at the same moment. */
.linelist { display: block; width: 100%; max-width: 520px; }
.lrow { position: relative; display: flex; align-items: baseline;
        justify-content: space-between; gap: 1.4em; padding: .62em 0; }
.lrow.tall { flex-direction: column; align-items: flex-start; gap: .35em;
             padding: 1em 0; }
.lrow > i {
  position: absolute; left: 0; right: 0; bottom: 0; height: 1px;
  background: currentColor; opacity: .26;
  transform: scaleX(0); transform-origin: 0 50%;
  transition: transform 1s cubic-bezier(.16,1,.3,1);
  transition-delay: calc(var(--i, 0) * 110ms);
}
.lk, .lv {
  display: inline-block; opacity: 0; transform: translateY(14px);
  transition: opacity .8s ease, transform .9s cubic-bezier(.16,1,.3,1);
}
.lk { transition-delay: calc(var(--i, 0) * 110ms); }
.lv { transition-delay: calc(var(--i, 0) * 110ms + 110ms); }
.linelist.in .lrow > i { transform: scaleX(1); }
.linelist.in .lk, .linelist.in .lv { opacity: 1; transform: none; }
.linelist.out .lrow > i { transform: scaleX(0); }
.linelist.out .lk, .linelist.out .lv { opacity: 0; transform: translateY(-14px); }

.lk { font-family: var(--body); font-weight: 700; font-size: 10px;
      letter-spacing: .2em; text-transform: uppercase; opacity: 0; }
.linelist.in .lk { opacity: .62; }
.lv { font-family: var(--display); font-size: clamp(15px, 1.5vw, 21px);
      font-variant-numeric: tabular-nums; }
.lrow.tall .lv { opacity: 0; }
.linelist.in .lrow.tall .lv { opacity: .5; }

/* No dashed rules anywhere. A dashed border renders as a row of typed hyphens
   at small sizes and reads as unfinished markup rather than as a divider. */
.ask, .hrs tr, .lrow { border: 0; }
@media (prefers-reduced-motion: reduce) {
  .lk, .lv { opacity: 1; transform: none; }
  .lrow > i { transform: scaleX(1); }
}


/* ------------------------------------------------------------ mini gallery --
   The reference's first gallery: a framed picture with a large title laid over
   it and a caption beneath the frame. Both belong to that picture, and the
   outgoing pair leaves before the incoming pair arrives, so there is never a
   moment with two titles on screen. */
.mini-sec .hold { display: grid; gap: clamp(14px, 2.2vh, 26px); justify-items: center; }
.mgal { display: grid; gap: clamp(14px, 2vh, 26px); width: 100%;
        grid-template-columns: minmax(0, 1fr); justify-items: center; }
/* the head: a very large centred title with the timer bar centred beneath it,
   the bar about half the width of the picture */
.mghead { display: grid; justify-items: center; gap: clamp(10px, 1.6vh, 20px);
          width: 100%; }
/* The reserved height has to be in the TITLE's own size. At 1.1em of the
   parent's 16px it was 17px tall while the title inside it was 104px, so the
   absolutely positioned title escaped upward over the section above. */
.mgtitles { position: relative; width: 100%; text-align: center;
            margin-top: clamp(10px, 1.8vh, 26px);
            font-size: clamp(30px, 6vw, 104px); min-height: 1.06em;
            line-height: 1; }
.mgbar { width: 50%; margin: calc(-1 * clamp(4px, .9vh, 14px)) auto 0;
         justify-content: center; }
/* The stage IS the picture. Everything in the chain is pinned to it explicitly:
   left to itself, the frame kept its 16/9 ratio and sized the whole stack to it,
   so the picture filled the left half and the darkening gradient covered the
   page behind the rest. */
.mgstage { position: relative; overflow: hidden; width: 100%;
           height: clamp(300px, 54vh, 620px); }
.mgal .shots { position: absolute; inset: 0; width: 100%; height: 100%; }
.mgal .shots > .shot { position: absolute; inset: 0; }
.mgal .shots > .shot .frame { position: absolute; inset: 0;
                              width: 100%; height: 100%; aspect-ratio: auto; }
.mgal .shots > .shot .frame > img { top: -6%; left: -6%; width: 112%; max-width: none; height: 112%; }

/* Every title in a gallery arrives and leaves LETTER BY LETTER, in both
   directions. A whole line appearing at once is a state change, and this is the
   one piece of type on the page that changes while the reader is watching it. */
.mgtitle { position: absolute; inset: 0; margin: 0; text-align: center;
           font-family: var(--display); font-weight: 700; text-transform: uppercase;
           font-size: inherit; line-height: 1.0; letter-spacing: -.015em; }
.mgtitle .c {
  opacity: 0; transform: rotateY(-90deg) translateY(26px);
  transition: opacity .42s ease calc(var(--i) * 34ms + .12s),
              transform .78s cubic-bezier(.16,1,.3,1) calc(var(--i) * 34ms);
}
.mgtitle.on .c { opacity: 1; transform: none; }
.mgtitle.leaving .c {
  opacity: 0; transform: rotateY(78deg) translateY(-22px);
  transition: opacity .3s ease calc(var(--i) * 22ms),
              transform .5s cubic-bezier(.4,0,.2,1) calc(var(--i) * 22ms);
}
.mgcaps { position: relative; width: min(52ch, 92%); min-height: 3.4em;
          text-align: center; }
.mgcap { position: absolute; inset: 0; margin: 0;
         font-size: clamp(14px, 1.15vw, 17px); line-height: 1.5; opacity: 0;
         transform: translateY(12px);
         transition: opacity .5s ease, transform .7s cubic-bezier(.16,1,.3,1); }
.mgcap.on { opacity: .74; transform: none; }
.mgcap.leaving { opacity: 0; transform: translateY(-12px); }
.mgtitle.leaving { opacity: 0; transform: translateY(-16px); }
.mgal .gctl { margin-top: 0; }


/* ------------------------------------------------------- travel between links --
   A jump to an anchor is a teleport: the page is simply somewhere else and the
   viewer has to work out where. The same arch that opened the site closes over
   the current view, the page moves behind it, and it opens again on the
   destination, so arriving somewhere else is a move rather than a cut. */
.enter.travel::before { transition: none; }
.enter.travel.closing::before {
  animation: archClose .74s cubic-bezier(.45,0,.2,1) forwards;
}
.enter.travel.opening::before {
  animation: archOpen 1.15s cubic-bezier(.2,0,.18,1) forwards;
}
@keyframes archClose {
  from { width: 340vmax; height: 380vmax; top: 50%; }
  to   { width: 26vmin;  height: 40vmin;  top: 57%; }
}
@keyframes archOpen {
  from { width: 26vmin;  height: 40vmin;  top: 57%; }
  to   { width: 340vmax; height: 380vmax; top: 50%; }
}
@media (prefers-reduced-motion: reduce) { .enter.travel { display: none; } }

/* ------------------------------------------------------------------ form --- */
/* Underlined fields rather than boxes: a bordered input control is the single
   most template-looking object on the internet, and the rest of this page is
   built out of rules and type. Colours are all mixed FROM currentColor for the
   same reason the labels are: this section renders on either ground. */
.form-sec .bigline { font-size: clamp(30px, 4.6vw, 68px); margin-bottom: .7em; }
.quoteform {
  /* start, not stretch: a grid row otherwise stretches a short input to the
     height of whatever sits beside it */
  display: grid; align-items: start; grid-template-columns: 1fr 1fr;
  gap: clamp(18px, 2.2vw, 30px) clamp(22px, 3vw, 44px);
  max-width: 820px; margin: 0;
}
.quoteform label { display: block; }
.quoteform label.wide,
.quoteform button,
.quoteform .formnote { grid-column: 1 / -1; }
.quoteform span {
  display: block; font-family: var(--body); font-weight: 700;
  font-size: clamp(9px, 0.72vw, 11px); letter-spacing: .22em;
  text-transform: uppercase; margin-bottom: .55em;
  color: color-mix(in srgb, currentColor 60%, transparent);
}
.quoteform input,
.quoteform textarea {
  width: 100%; background: transparent; color: inherit;
  font-family: var(--body); font-size: clamp(16px, 1.25vw, 19px);
  line-height: 1.5; border: 0;
  border-bottom: 1px solid color-mix(in srgb, currentColor 34%, transparent);
  padding: .35em 0 .5em; border-radius: 0;   /* iOS rounds inputs by default */
  transition: border-color .35s cubic-bezier(.22,.61,.36,1);
}
.quoteform textarea { resize: vertical; min-height: 3.4em; }
.quoteform input:focus,
.quoteform textarea:focus {
  outline: none; border-bottom-color: currentColor;
}
.quoteform input:user-invalid,
.quoteform textarea:user-invalid { border-bottom-color: var(--accent, currentColor); }
.quoteform button {
  justify-self: start; font-family: var(--body); font-weight: 700;
  font-size: clamp(10px, 0.8vw, 12px); letter-spacing: .22em;
  text-transform: uppercase; color: inherit; cursor: pointer;
  background: transparent; padding: 1.05em 2.4em;
  border: 1px solid color-mix(in srgb, currentColor 45%, transparent);
  border-radius: 0;
  /* every interactive animation on this site is eased, never snapped */
  transition: background .5s cubic-bezier(.22,.61,.36,1),
              color .5s cubic-bezier(.22,.61,.36,1),
              border-color .5s cubic-bezier(.22,.61,.36,1);
}
.quoteform button:hover,
.quoteform button:focus-visible {
  background: currentColor; border-color: currentColor;
  color: var(--ground);
}
.quoteform button[disabled] { opacity: .45; cursor: default; }
.formnote {
  margin: 0; font-size: clamp(13px, 1vw, 15px); max-width: 52ch;
  min-height: 1.5em; opacity: 0;
  color: color-mix(in srgb, currentColor 72%, transparent);
  transition: opacity .5s cubic-bezier(.22,.61,.36,1);
}
.formnote.show { opacity: 1; }
.formalt { margin-top: clamp(26px, 3.4vh, 44px); max-width: 46ch; }
.formalt a { text-decoration: none; border-bottom: 1px solid currentColor; }
@media (max-width: 720px) {
  .quoteform { grid-template-columns: 1fr; }
}
