/* ============================================================================
   motion.css — the premium layer. Components injected/driven by motion.js.
   Everything here is additive: delete this file + motion.js and the site
   still works on base.css alone. See docs/animation-system.md.
   ============================================================================ */

/* ---------- film grain (#grain injected by motion.js) ---------- */
#grain{position:fixed;inset:-100px;z-index:90;pointer-events:none;opacity:.05;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  animation:grain 1.1s steps(5) infinite}
@keyframes grain{
  0%{transform:translate(0,0)}20%{transform:translate(-34px,22px)}
  40%{transform:translate(18px,-40px)}60%{transform:translate(-46px,-8px)}
  80%{transform:translate(30px,34px)}100%{transform:translate(0,0)}}

/* ---------- scroll progress hairline (#progress) ---------- */
#progress{position:fixed;top:0;left:0;right:0;height:2px;z-index:80;pointer-events:none}
#progress i{display:block;height:100%;background:var(--accent);transform:scaleX(0);transform-origin:left}

/* ---------- custom cursor (#cur, #cur-ring) — fine pointers only ---------- */
html.cursor-on body{cursor:none}
html.cursor-on a,html.cursor-on button,html.cursor-on .rail{cursor:none}
html.cursor-on input,html.cursor-on textarea,html.cursor-on select{cursor:auto}
#cur,#cur-ring{position:fixed;left:0;top:0;z-index:210;pointer-events:none;border-radius:50%;opacity:0;transition:opacity .4s}
html.cursor-live #cur,html.cursor-live #cur-ring{opacity:1}
#cur{width:5px;height:5px;background:var(--accent);margin:-2.5px 0 0 -2.5px}
#cur-ring{width:34px;height:34px;border:1px solid color-mix(in srgb,var(--accent) 62%,transparent);margin:-17px 0 0 -17px;
  display:grid;place-items:center;
  transition:opacity .4s,width .35s var(--ease-out),height .35s var(--ease-out),margin .35s var(--ease-out),background-color .35s,border-color .35s}
#cur-ring::before{content:attr(data-glyph);font:400 11px/1 sans-serif;letter-spacing:.28em;text-indent:.28em;color:var(--ink);opacity:0;transition:opacity .25s}
/* states set by motion.js */
html[data-cursor="link"] #cur-ring{width:52px;height:52px;margin:-26px 0 0 -26px;background:color-mix(in srgb,var(--accent) 16%,transparent)}
html[data-cursor="link"] #cur{opacity:0}
html[data-cursor="drag"] #cur-ring{width:64px;height:64px;margin:-32px 0 0 -32px;background:var(--accent);border-color:var(--accent)}
html[data-cursor="drag"] #cur-ring::before{opacity:1}
html[data-cursor="drag"] #cur{opacity:0}

/* ---------- section dots nav (#dots) ---------- */
#dots{position:fixed;right:clamp(14px,2vw,30px);top:50%;transform:translateY(-50%);z-index:70;display:flex;flex-direction:column;gap:14px}
#dots a{position:relative;width:26px;height:10px;display:flex;align-items:center;justify-content:flex-end}
#dots a i{width:5px;height:5px;border-radius:50%;background:currentColor;opacity:.32;transition:opacity .4s,transform .4s var(--ease-out)}
#dots a.on i{opacity:1;transform:scale(1.7);background:var(--accent)}
#dots a b{position:absolute;right:32px;top:50%;transform:translate(8px,-50%);font-weight:400;font-size:9.5px;letter-spacing:.26em;text-transform:uppercase;white-space:nowrap;opacity:0;transition:opacity .35s,transform .35s var(--ease-out);pointer-events:none}
#dots a:hover b,#dots a.on:hover b{opacity:.9;transform:translate(0,-50%)}
@media(max-width:1080px){#dots{display:none}}

/* ---------- floating hover preview (#preview) ---------- */
#preview{position:fixed;left:0;top:0;z-index:65;width:min(330px,26vw);aspect-ratio:16/10;pointer-events:none;
  background-size:cover;background-position:center;opacity:0;transform:scale(.86);
  transition:opacity .45s ease,transform .55s var(--ease-out);will-change:transform}
#preview.show{opacity:1;transform:scale(1)}
@media(max-width:1080px){#preview{display:none}}

/* ---------- giant outline marquee ---------- */
.mq-big{-webkit-mask-image:none;mask-image:none}
.mq-big .mq-t{gap:clamp(40px,6vw,110px);padding-right:clamp(40px,6vw,110px);animation-duration:70s}
.mq-big .pull{font-size:clamp(56px,11vw,170px);white-space:nowrap}
.mq-big .pull::after{display:none}
.mq-big .outline{color:transparent;-webkit-text-stroke:1px var(--stroke,currentColor)}

/* ---------- placeholder art for CGI still in production ---------- */
.ph-art{position:relative;overflow:hidden;background:
  repeating-linear-gradient(90deg,color-mix(in srgb,var(--accent) 22%,transparent) 0 1px,transparent 1px calc(100%/9)),
  linear-gradient(165deg,var(--sunk) 0%,var(--ink-2,var(--sunk)) 70%)}
.ph-art.r-32{aspect-ratio:3/2}.ph-art.r-1610{aspect-ratio:16/10}
.ph-art::before{content:"";position:absolute;inset:0;transform:translateX(-101%) skewX(-14deg);
  background:linear-gradient(90deg,transparent,color-mix(in srgb,var(--accent) 9%,transparent),transparent);
  animation:sheen 5.5s var(--ease-io) infinite}
@keyframes sheen{0%,55%{transform:translateX(-101%) skewX(-14deg)}90%,100%{transform:translateX(101%) skewX(-14deg)}}
.ph-art::after{content:"";position:absolute;inset:clamp(10px,1.6vw,18px);border:1px solid color-mix(in srgb,var(--accent) 26%,transparent)}
.ph-art .ph-tag{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:2;
  font-size:9px;letter-spacing:.3em;text-transform:uppercase;color:var(--accent-2,var(--accent));opacity:.85;white-space:nowrap}
.ph-art .ph-name{position:absolute;left:clamp(16px,2.4vw,26px);bottom:clamp(12px,2vw,20px);z-index:2;
  font-size:10.5px;letter-spacing:.22em;text-transform:uppercase;color:var(--muted)}

/* ---------- Downtown vertical grid draws in after the preloader ---------- */
/* The brand device draws UPWARD out of the skyline — "where ambition becomes
   visible" — and carries a warm charge that decays to its resting hairline. */
body.ready .hero .vgrid{transform:scaleY(1)}
.hero .vgrid{transform:scaleY(0);transform-origin:bottom;transition:transform 1.9s var(--ease-io) .55s}
.hero .vgrid::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:inherit;filter:brightness(2.6) saturate(.4);opacity:0;
  transition:opacity 1.5s ease 2.1s}
body.ready .hero .vgrid::after{opacity:0}
body.hero-charge .hero .vgrid::after{opacity:.85;transition:opacity .5s ease .55s}

/* ---------- hero content fades up and away on scroll (driven by JS) ---------- */
.hero-in{will-change:transform,opacity}

/* ---------- reduced motion / no-JS ---------- */
@media(prefers-reduced-motion:reduce){
  #grain,#cur,#cur-ring,#preview{display:none}
  .ph-art::before{animation:none}
  .hero .vgrid{transform:none;transition:none}
  .mq-big .mq-t{animation:none}
}
html:not(.js) .hero .vgrid{transform:none}

/* ============================================================================
   SCRUB LAYER — pinned scenes, expanding figures, section covers.
   Mechanics modelled on the reference sites; implementation is native
   position:sticky + the shared ticker. No scroll-jacking.
   ============================================================================ */

/* ---------- pinned scene shell ---------- */
.pin-wrap{position:relative;height:var(--pinlen,520vh)}
/* declared here, not inline, so the phone override can win */
#podium-life{--pinlen:460vh}   /* 4 slides x 100vh + 60vh */
/* The pin owns the ground colour. pins() cross-fades --pin-* between the
   grounds of the two slides straddling the centre, so Dining and Wellness melt
   into each other instead of meeting at a hard vertical edge. */
.pin{position:sticky;top:0;height:100svh;overflow:hidden;display:flex;flex-direction:column;justify-content:center;
  background:var(--ink,#16170F)}

/* ---------- full-screen horizontal gallery ---------- */
.gal-head{position:absolute;top:0;left:0;right:0;z-index:5;display:flex;align-items:flex-start;justify-content:space-between;gap:24px;padding:clamp(86px,11vh,120px) clamp(20px,4vw,60px) 0;pointer-events:none}
.gal-title{font-family:var(--display);font-variation-settings:var(--wide,normal);font-weight:400;text-transform:uppercase;font-size:clamp(22px,3vw,44px);line-height:1;letter-spacing:-.01em;color:var(--onmedia)}
.gal-meta{display:flex;align-items:center;gap:16px;font-variant-numeric:tabular-nums;color:var(--onmedia)}
.gal-meta .c{font-family:var(--display);font-size:clamp(20px,2.2vw,32px);font-weight:300}
.gal-meta .of{font-size:10px;letter-spacing:.24em;opacity:.55}
.gal-prog{width:clamp(70px,9vw,150px);height:1px;background:rgba(240,235,231,.25);overflow:hidden}
.gal-prog i{display:block;height:100%;background:var(--accent-2,var(--accent));transform:scaleX(0);transform-origin:left}

.htrack{display:flex;height:100%;will-change:transform}
.gal-slide{position:relative;flex:0 0 100vw;height:100%;overflow:hidden;background:var(--ink)}
.gal-slide > .gs-media{position:absolute;inset:0 -10%;background-size:cover;background-position:center;will-change:transform}
.gal-slide > .gs-media img{width:100%;height:100%;object-fit:cover}
.gal-slide::after{content:"";position:absolute;inset:0;z-index:1;background:linear-gradient(180deg,rgba(20,21,16,.42) 0%,rgba(20,21,16,.06) 40%,rgba(20,21,16,.66) 100%)}
.gs-body{position:absolute;left:clamp(20px,4vw,60px);right:clamp(20px,4vw,60px);bottom:clamp(28px,6vh,72px);z-index:2;color:var(--onmedia);will-change:transform,opacity}
.gs-i{display:block;font-family:var(--display);font-variation-settings:var(--wide,normal);font-weight:300;font-size:clamp(15px,1.4vw,20px);letter-spacing:.14em;color:var(--accent-2,var(--accent));margin-bottom:12px}
.gs-t{font-family:var(--display);font-variation-settings:var(--wide,normal);font-weight:400;text-transform:uppercase;font-size:clamp(34px,6.4vw,104px);line-height:.98;letter-spacing:-.015em}
.gs-p{margin-top:16px;max-width:46ch;font-size:clamp(12.5px,1vw,14.5px);line-height:1.75;color:rgba(240,235,231,.82)}
.gal-hint{position:absolute;right:clamp(20px,4vw,60px);bottom:clamp(28px,6vh,72px);z-index:3;font-size:9.5px;letter-spacing:.3em;text-transform:uppercase;color:rgba(240,235,231,.55);display:flex;align-items:center;gap:12px}
.gal-hint i{width:clamp(26px,4vw,54px);height:1px;background:currentColor}
/* the hint has to name the actual gesture: the desktop pin advances on the
   vertical scroll, the phone rail on a sideways swipe */
.hint-swipe{display:none}

/* per-character title rise (used in the gallery head) */
.split-chars{overflow:hidden}
.split-chars .wd{display:inline-block;white-space:nowrap}
.split-chars .ch{display:inline-block;transform:translateY(115%);transition:transform .9s var(--ease-out)}
.split-chars.on .ch{transform:none}

/* ---------- expanding figure ([data-expand], --xp 1->0) ---------- */
[data-expand]{--xp:1;clip-path:inset(0 calc(var(--xp)*11%) round calc(var(--xp)*4px));transform:translateZ(0)}
[data-expand] img{transform:scale(calc(1 + var(--xp)*.16));transform-origin:center;will-change:transform}

/* ---------- hero cover: the next section slides over the pinned hero.
   .hero-run gives a 2-viewport runway so the hero releases once covered
   (it stops painting/compositing beneath the rest of the page). ---------- */
.hero-cover .hero-run{height:200svh}
.hero-cover .hero-run .hero{position:sticky;top:0}
.hero-cover .hero-run + *{position:relative;z-index:3}

/* ---------- fallbacks: small screens + reduced motion + no JS ----------
   The pinned gallery becomes a native swipe rail; expand un-clips.  */

/* ============================================================================
   THE PHONE RAIL — why the pin does not run here
   ---------------------------------------------------------------------------
   A pinned scrub drives the track from a scroll offset that JavaScript reads
   and writes every frame. On a desktop wheel that is invisible, because the
   site already eases the scroll itself and the two agree. Under a finger it
   cannot win: iOS composites its own momentum on a separate thread and throttles
   scroll events while it runs, so a JS-driven track always arrives a frame or
   two behind the thumb. That lag is what read as "messed up" — plus 620vh of
   scrolling during which the page appeared frozen.

   So on phones the gallery is a real scroll container. The OS moves the cards,
   which is the one thing guaranteed to track the finger exactly, momentum and
   rubber-band included. JavaScript is left with the counter and the progress
   bar only — small UI where a late frame cannot be seen — and the reveal runs
   on a view() timeline, off the main thread.

   Each card is FULL BLEED: one viewport wide, one viewport tall, the picture
   edge to edge. The header and the hint float over it on the slide's own scrim,
   exactly as they do on desktop. An inset card left the photographs floating in
   a field of ink and wasted the half of the screen a render most needs.

   That costs the peek — at 100vw there is no neighbour showing — so the
   sideways axis is announced instead: the counter reads 01 / 04, the progress
   line fills, and the hint says SWIPE. The vertical snap below lands the whole
   thing on the screen at once, which is what makes those three legible.
   ============================================================================ */
@media (max-width:900px){
  /* the runway collapses: no pin, no dead scroll */
  #podium-life{--pinlen:auto}
  .pin-wrap{height:auto}
  /* One viewport tall and it CLIPS, because the cards inside are full bleed.
     svh, not dvh or lvh: svh is the height visible with the browser chrome
     shown, so the section is never taller than the screen at its smallest.
     When the address bar retracts the viewport grows and the section sits
     centred in it — stable, where dvh relayouts on every bar transition. */
  .pin{position:relative;height:100svh;overflow:hidden;display:block;padding:0}

  /* Header and hint float over the picture again, on the slide's own scrim —
     the same arrangement as desktop. They were pulled into flow when the cards
     were inset and there were two headlines fighting for the top-left corner;
     full-bleed photographs have no competing title, so they go back over. */
  .gal-head{position:absolute;padding:clamp(58px,13vw,82px) clamp(20px,6vw,34px) 0;
    align-items:flex-start;gap:16px}
  /* "/ 04" broke across two lines once the title took the width it needed */
  .gal-meta{flex:0 0 auto;flex-wrap:nowrap;white-space:nowrap}
  .gal-meta .of{display:inline}

  /* The type now sits over a photograph at every moment, and some of these
     renders are a bright morning sky. The stock scrim — .42 at the top, fading
     out by 40% — left the eyebrow at 2.1:1 against the Sport render, which is
     unreadable. Deepened at the head, and the fade pushed lower so the title
     is still covered where it wraps to a second line. */
  .gal-slide::after{background:linear-gradient(180deg,
    rgba(20,21,16,.80) 0%, rgba(20,21,16,.60) 18%, rgba(20,21,16,.16) 40%,
    rgba(20,21,16,.30) 68%, rgba(20,21,16,.80) 100%)}
  .gal-meta .c{font-size:clamp(17px,5vw,22px)}
  .gal-prog{width:clamp(48px,13vw,80px)}

  .htrack{overflow-x:auto;overflow-y:hidden;scroll-snap-type:x mandatory;
    transform:none!important;will-change:auto;
    -webkit-overflow-scrolling:touch;
    /* a sideways swipe must never chain into the page or the back gesture */
    overscroll-behavior-x:contain;
    scrollbar-width:none;flex-wrap:nowrap;height:100%;
    gap:0;padding-inline:0;scroll-padding-inline:0}
  .htrack::-webkit-scrollbar{display:none}
  /* one screen wide, one screen tall, no gap and no radius — the picture is
     the card */
  .htrack .gal-slide{flex:0 0 100vw;width:100vw;height:100%;aspect-ratio:auto;
    scroll-snap-align:center;scroll-snap-stop:always;border-radius:0}

  /* the pin's per-frame writes never happen here, so clear their resting state */
  .gal-slide > .gs-media{inset:0}
  .gal-slide > .gs-media img{width:100%;height:100%;object-fit:cover}
  .gs-body{position:absolute;left:clamp(20px,6vw,34px);right:clamp(20px,6vw,34px);
    bottom:clamp(58px,14vw,86px)}
  .gs-t{font-size:clamp(34px,9vw,54px)}
  .gs-p{margin-top:12px;font-size:13px;line-height:1.65;max-width:34ch}

  .gal-hint{position:absolute;right:clamp(20px,6vw,34px);bottom:clamp(22px,5.5vw,34px);
    margin:0;font-size:8.5px;letter-spacing:.24em}
  .gal-hint i{width:22px}
  .hint-scroll{display:none}
  .hint-swipe{display:inline}

  /* the head sits on the section's own dark ground, so it must NOT follow the
     --pin-* interpolation the desktop scrub writes onto .pin */
  .pin .gal-title,.pin .gal-meta,.pin .gal-meta .c{color:var(--onmedia)}
  .pin .gal-hint{color:rgba(240,235,231,.55)}
  .pin .gal-head .eyebrow{color:var(--accent-2)!important}
  .pin .gal-prog{background:rgba(240,235,231,.25);opacity:1}
  .pin .gal-prog i{background:var(--accent-2,var(--accent))}

  .split-chars .ch{transform:none;transition:none}
  [data-expand]{--xp:0!important;clip-path:none}
  [data-expand] img{transform:none}
  .hero-cover .hero-run{height:auto}
  .hero-cover .hero-run .hero{position:relative}
}


/* ============================================================================
   THE VERTICAL SNAP — one viewport, then a push sideways
   ---------------------------------------------------------------------------
   On a phone the gallery is a sideways rail inside a page that scrolls down.
   Nothing tells the reader to change axis. Snapping the section to fill the
   screen is that cue: the vertical scroll settles, the composition sits whole
   in front of them with the next card peeking, and the only place left to go
   is across.

   `proximity`, never `mandatory`. Mandatory would pull the page toward this
   one snap point from a long way off and make the rest of the page feel
   sticky; proximity only engages when a scroll already ends near the section,
   so carrying on downward stays completely free.

   Scoped to a coarse pointer because the site's own smooth scroll runs the
   wheel through a lerp on fine pointers — CSS snap and a JS-driven scrollTo
   would fight for the same frames. Touch uses the native scroller, where snap
   is the browser's job and costs nothing.
   ============================================================================ */
/* Orientation no longer matters. The portrait gate existed because a stacked
   header plus an inset card could not fit a landscape phone's ~390px of
   height; a full-bleed picture with the header floating over it fits any
   viewport, so a turned phone gets the same screen-filling card and the same
   snap. */
@media (max-width:900px) and (pointer:coarse) and (prefers-reduced-motion:no-preference){
  html{scroll-snap-type:y proximity}
  /* centre, not start: if the viewport grows when the address bar retracts,
     the section stays balanced in it instead of hanging off the bottom */
  #podium-life{scroll-snap-align:center;scroll-snap-stop:normal}
}

/* Short screens — a small phone, or any phone turned sideways. The card fills
   the viewport whatever its height, so nothing has to shrink to make room; the
   header simply moves in from the edges by less, and the caption with it, or
   the two would meet in the middle of a 390px-tall screen. */
@media (max-width:900px) and (max-height:680px){
  .gal-head{padding:clamp(46px,9vh,64px) clamp(20px,4vw,30px) 0}
  .gal-title{font-size:clamp(18px,4.4vh,26px);line-height:1.06;margin-top:8px!important}
  .gal-meta .c{font-size:17px}
  .gs-body{left:clamp(20px,4vw,30px);right:clamp(20px,4vw,30px);bottom:clamp(34px,9vh,52px)}
  .gs-t{font-size:clamp(26px,7.6vh,42px)}
  .gs-p{margin-top:8px;font-size:12px;line-height:1.55}
  .gal-hint{right:clamp(20px,4vw,30px);bottom:clamp(14px,3.4vh,22px)}
}

/* Card reveal on a view() timeline — the compositor runs it, so it cannot fall
   behind the scroll the way a scroll-event handler does. Unsupported browsers
   simply get the finished state, which is a complete design on its own. */
@supports (animation-timeline:view(inline)){
  @media (max-width:900px) and (prefers-reduced-motion:no-preference){
    /* The CARD is no longer animated. It used to scale to .955 at the edges,
       which was fine while cards were inset with a gap between them — at full
       bleed the same scale opens a 4.5% strip of bare ink between neighbours
       mid-swipe. The picture and the caption carry the motion instead. */
    .htrack .gal-slide > .gs-media img{
      animation:railInner linear both;
      animation-timeline:view(inline);
      animation-range:entry 0% exit 100%}
    .htrack .gal-slide .gs-body{
      animation:railBody linear both;
      animation-timeline:view(inline);
      animation-range:entry 20% exit 80%}
  }
}
/* the caption settles as its card arrives and lets go as it leaves — the same
   distance-from-centre reveal the desktop scrub does, expressed as a timeline */
@keyframes railBody{
  0%  {opacity:0;transform:translate3d(0,26px,0)}
  38% {opacity:1;transform:none}
  62% {opacity:1;transform:none}
  100%{opacity:0;transform:translate3d(0,26px,0)}
}
/* a slow counter-drift inside the frame: the picture reads as deeper than the
   card it sits in, which is the whole trick of a parallax gallery */
@keyframes railInner{
  0%  {transform:translate3d(6%,0,0) scale(1.1)}
  100%{transform:translate3d(-6%,0,0) scale(1.1)}
}
@media (prefers-reduced-motion:reduce){
  .pin-wrap{height:auto!important}
  .pin{position:relative;height:auto;overflow:visible;background:var(--ink,#16170F)}
  .gal-head{position:relative;padding-top:clamp(88px,12vw,120px)}
  .gal-meta{display:none}
  .htrack{overflow-x:auto;scroll-snap-type:x mandatory;transform:none!important;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex-wrap:nowrap}
  .htrack::-webkit-scrollbar{display:none}
  .htrack .gal-slide{flex:0 0 86vw;height:72svh;scroll-snap-align:center}
  /* the hint drops into flow here too, or it would float over the last card */
  .gal-hint{position:relative;right:auto;bottom:auto;justify-content:flex-end;
    margin:20px clamp(20px,4vw,60px) 0}
  .gs-body{opacity:1!important;transform:none!important}
  .gal-slide > .gs-media{inset:0}
  .gal-slide > .gs-media img{transform:none!important}
  .hint-scroll{display:none}
  .hint-swipe{display:inline}
  .split-chars .ch{transform:none;transition:none}
  [data-expand]{--xp:0!important;clip-path:none}
  [data-expand] img{transform:none}
  .hero-cover .hero-run{height:auto}
  .hero-cover .hero-run .hero{position:relative}
}
html:not(.js) .pin-wrap{height:auto}
html:not(.js) .pin{position:relative;height:auto}
html:not(.js) .htrack{overflow-x:auto}
html:not(.js) .split-chars .ch{transform:none}
html:not(.js) [data-expand]{clip-path:none}

/* ============================================================================
   EDITORIAL COLLAGE LAYER — scattered imagery at independent parallax speeds,
   script + caps titles overlapping the pictures. Driven by [data-collage] /
   [data-cg] in motion.js.

   Layout lives HERE, not in the markup: each stage child carries a positional
   class (.c1 ... .c5, plus .cg-title / .cg-copy) and every section declares
   two compositions — one for wide screens, one for phones. That is the whole
   point of the split: a phone is not a narrow desktop, so it gets its own art
   direction rather than the desktop grid squashed. See the COMPOSITIONS
   chapter at the bottom of this file.
   ============================================================================ */
/* The stage is the clip boundary for the whole composition. Pictures sit flush
   to — and on phones deliberately past — both edges, and the parallax drifts
   them further; without this the wide layout opens a 21px horizontal scrollbar
   on #wellness. `clip` rather than `hidden` because it creates no scroll
   container, so overflow-y stays visible and vertical drift is never cut. */
.collage .cg-stage{position:relative;min-height:170vh;overflow-x:clip;overflow-y:visible}
.cg-item{position:absolute;margin:0;will-change:transform}
.cg-item.media{background:var(--sunk)}
/* stock-city-02 is a night shot at mean luma 28 against an ink ground — without
   an edge it reads as a hole in the page rather than a picture */
.dark .cg-item.media{box-shadow:0 0 0 1px rgba(240,235,231,.10)}
.cg-copy{position:absolute;z-index:5;max-width:19rem;font-size:clamp(12.5px,1vw,14.5px);line-height:1.8}
.cg-copy .link-u{margin-top:18px}

/* script + caps title, layered over the images */
.cg-title{position:absolute;z-index:4;pointer-events:none}
/* The fallback chain, and why it is this long. 'Altesse' resolves to nothing:
   the .otf supplied for it is corrupt — well-formed sfnt header, garbage table
   directory, offsets past the end of the file — and its @font-face was removed
   on 26 Aug 2026. The master in the brand folder is byte-identical, so it was
   never a usable web font, and until a licensed woff2 exists this line is
   always painted by a fallback.
   Snell Roundhand and Savoye LET are both macOS-only. Everyone on this team is
   on a Mac, so nobody had seen what the chain did next: it reached generic
   `cursive`, which on Windows is Comic Sans MS — at clamp(58px,10.5vw,190px),
   over the service collage, on a Trump-branded property.
   Segoe Script ships with Windows and Brush Script MT with Office; both are
   script faces, which is the intent. `cursive` stays last as a true fallback
   rather than the usual outcome. Whether this line should carry a script face
   at all is open — see docs/design-reviews/open-questions.md §3 — and is a
   design decision, so it is not taken here. */
.script-line{display:block;font-family:'Altesse','Snell Roundhand','Savoye LET','Segoe Script','Brush Script MT',cursive;
  font-weight:400;font-size:clamp(58px,10.5vw,190px);line-height:.9;white-space:nowrap;
  clip-path:inset(-25% 102% -25% -2%);transition:clip-path 1.7s var(--ease-io) .25s}
.in-view .script-line{clip-path:inset(-25% -6% -25% -2%)}
.caps-line{display:block;font-size:clamp(40px,7.4vw,132px);line-height:.94;
  text-transform:uppercase;letter-spacing:.02em}
.cg-title .caps-line{margin-top:-.14em;margin-left:.85em}

/* Reduced motion keeps the composition and loses only the drift (the job in
   motion.js bails on RM). No JS gets the same static composition. */
html:not(.js) .script-line{clip-path:none}


/* ============================================================================
   COLLAGE COMPOSITIONS
   ---------------------------------------------------------------------------
   Percentages are of the stage: `left` of its width (it is full-bleed, so of
   the viewport), `top` of its height. Values beyond 0-100% are deliberate —
   that is how a picture bleeds off the edge.

   Two compositions per section. The phone one is not a shrunk copy: pictures
   grow to 50-75vw, stagger left/right, bleed past one edge and overlap each
   other, so the section still reads as an editorial spread rather than a
   stack of equal boxes. Keep `sizes` on each <img> in step with the widths
   below, or the browser fetches the wrong variant.
   ============================================================================ */

/* ------------------------------------------------- wide and landscape ---- */
/* the default composition; the phone block below overrides it in portrait */

/* DINING — the anchor picture is a full-height column hard against the left
   edge, the title crossing its right side, and a staggered trio to the right
   of it. The eye travels left-to-right and down. */
#dining   .cg-stage{min-height:175vh}
#dining   .c1{left:0;top:2%;width:40%;height:100vh}
#dining   .c2{left:47%;top:5%;width:19%;aspect-ratio:4/5}
#dining   .c3{left:74%;top:12%;width:22%;aspect-ratio:4/5}
#dining   .c4{left:56%;top:62%;width:26%;aspect-ratio:3/2}
/* The title sits on the ground, NOT on the anchor picture. At left:13% half of
   "DINING" — set in near-black for the cream ground — fell across the night
   photograph and was unreadable at every scroll position (measured: 48-51% of
   the caps line over the picture across the whole traverse, the drift never
   resolving it). Wellness never had the fault because its title occupies the
   free ground between the left cluster and the right anchor; dining now does
   the same on its own side. The pair still mirrors: anchor and title always on
   opposite halves. */
#dining   .cg-title{left:42%;top:41%}
/* Dining's title sits in the gap between the anchor picture and the right edge
   — about 58% of the stage — where Wellness's has the whole left half. A
   narrower slot takes a smaller size: at the shared clamp the Russian
   "ГАСТРОНОМИЯ" ran 49px past the edge at 1440 and was clipped without a
   scrollbar to warn anyone. Sized to the longest word the site has to set. */
#dining   .cg-title{max-width:57%}
#dining   .caps-line{font-size:clamp(34px,6.2vw,110px)}
#dining   .script-line{font-size:clamp(30px,4.4vw,74px)!important}
#dining   .cg-copy{left:4.5%;top:80%}

/* WELLNESS — mirrored, so the pair reads as a spread rather than a repeat:
   the full-height anchor sits right, the cluster and title left. */
#wellness .cg-stage{min-height:175vh}
#wellness .c1{left:58%;top:0;width:42%;height:102vh}
#wellness .c2{left:6%;top:8%;width:26%;aspect-ratio:4/3}
#wellness .c3{left:38%;top:6%;width:14%;aspect-ratio:1/1}
#wellness .c4{left:8%;top:60%;width:34%;aspect-ratio:16/10}
#wellness .c5{left:46%;top:76%;width:15%;aspect-ratio:4/3}
#wellness .cg-title{left:10%;top:38%}
#wellness .cg-copy{left:70%;top:80%}

#service  .cg-stage{min-height:172vh}
#service  .c1{left:0;top:0;width:42%;height:104vh}
#service  .c2{left:47%;top:9%;width:21%;aspect-ratio:4/5}
#service  .c3{left:76%;top:3%;width:20%;aspect-ratio:3/4}
#service  .c4{left:68%;top:56%;width:24%;aspect-ratio:4/5}
#service  .c5{left:48%;top:72%;width:16%;aspect-ratio:3/2}
#service  .cg-title{left:38%;top:30%}
#service  .cg-copy{left:4.5%;top:78%}

/* ------------------------------------------- readability, any small screen -- */
@media (max-width:900px){
  /* max-width goes only where the copy is a full-width block: the portrait
     composition sets `width:88%`. In landscape the wide composition still
     places it as a column, and unbounded it ran under the pictures. */
  .cg-copy{font-size:15px;line-height:1.72}
  @media (max-aspect-ratio:1/1){ .cg-copy{max-width:none} }
  .cg-copy .link-u{margin-top:15px}
  .script-line{font-size:clamp(46px,14vw,86px)}
  /* Sized for the LONGEST word the site has to set, not the English one. At
     12.5vw the Russian "ГАСТРОНОМИЯ" ran 16px past the viewport at every phone
     width and was silently cut by the stage's overflow clip — no scrollbar, no
     warning, just a missing letter. Georgian sets wide too. */
  .caps-line{font-size:clamp(30px,10.4vw,62px)}
  /* max-width alone cannot hold a nowrap line — the Russian title still ran
     11px past a 820px tablet. Letting the script wrap costs nothing: it only
     ever breaks when it would not have fitted, and two short script lines over
     the caps read better than one line pushed off the screen. */
  .cg-title{max-width:92vw}
  .script-line{white-space:normal}
  .cg-title .caps-line{margin-left:.4em;margin-top:-.05em}
}

/* ------------------------------------------- wide composition, short screens -- */
/* The wide composition assumes a stage roughly as tall as it is wide — that is
   what a 175vh stage gives on a laptop or a desktop. A landscape phone at
   844x390 makes the same rule only 683px tall against 844px of width, so every
   picture (sized by a vw width plus an aspect-ratio) grows proportionally
   taller inside a shorter stage and the composition closes up. Pinning the
   stage to the viewport WIDTH there restores the proportion the layout is
   drawn for. */
@media (max-width:900px) and (min-aspect-ratio:1/1){
  #dining   .cg-stage{min-height:110vw}
  #wellness .cg-stage{min-height:110vw}
  #service  .cg-stage{min-height:108vw}
  /* The display sizes come back down too. The wide composition puts the dining
     title in the gap to the right of the anchor picture — about 490px of it in
     an 820px window — and at the small-screen caps size the Russian
     "ГАСТРОНОМИЯ" did not fit that gap. Sized to the longest word again. */
  .script-line{font-size:clamp(28px,4.4vw,44px)}
  .caps-line{font-size:clamp(24px,5.6vw,48px)}
  /* The anchor pictures are the one thing in the wide composition sized in vh.
     Once the stage is measured in vw they stop agreeing with it: at 820x800 a
     100vh anchor filled 89% of a 110vw stage instead of 57%, and swallowed the
     copy. Restated in vw, at the same fraction of the stage as on a desktop. */
  #dining   .c1{height:63vw}
  #wellness .c1{height:64vw}
  #service  .c1{height:65vw}
}

/* --------------------------------------------- phone composition, portrait -- */
/* Landscape phones keep the wide composition — it already suits a short, broad
   viewport. The portrait guard also matters because the stage height below is
   width-relative: on a wide-but-short screen a vw height would run to several
   viewports. */
@media (max-width:900px) and (max-aspect-ratio:1/1){
  /* Stage heights are in vw, not vh, on purpose. Every picture here is sized
     by a vw width plus an aspect-ratio, so its height scales with the viewport
     WIDTH; a vh stage would drift out of step with its own contents and the
     copy would collide with a picture on a tablet. In vw the whole composition
     is one fixed-proportion canvas. */

  /* DINING — the anchor swings right and grows to 72vw, the coffee bleeds off
     the left under it, then the lower pair staggers back. Nothing is centred
     and nothing shares an edge with its neighbour. */
  #dining   .cg-stage{min-height:265vw}
  #dining   .cg-title{left:6%;top:2%}
  #dining   .c1{height:auto;left:28%;top:15%;width:72%;aspect-ratio:3/4}
  #dining   .c2{left:-8%;top:38%;width:50%;aspect-ratio:4/5}
  #dining   .c3{left:44%;top:53%;width:46%;aspect-ratio:3/4}
  #dining   .c4{left:3%; top:67%;width:44%;aspect-ratio:3/2}
  #dining   .cg-copy{left:6%;top:84%;width:88%}

  /* WELLNESS — the same rhythm mirrored: anchor right, sauna bleeding off the
     left, then three descending steps back across the page. */
  #wellness .cg-stage{min-height:285vw}
  #wellness .cg-title{left:6%;top:2%}
  #wellness .c1{height:auto;left:26%;top:13%;width:74%;aspect-ratio:3/4}
  #wellness .c2{left:-6%;top:35%;width:52%;aspect-ratio:4/3}
  #wellness .c3{left:56%;top:52%;width:36%;aspect-ratio:1/1}
  #wellness .c4{left:2%; top:57%;width:56%;aspect-ratio:16/10}
  #wellness .c5{left:44%;top:70%;width:44%;aspect-ratio:4/3}
  #wellness .cg-copy{left:6%;top:86%;width:88%}

  /* SERVICE — the reference composition: tall portrait high and right, a
     landscape cutting under it from the left, then a staggered lower pair. */
  #service  .cg-stage{min-height:329vw}
  #service  .cg-title{left:6%;top:2%}
  #service  .c1{height:auto;left:30%;top:12%;width:72%;aspect-ratio:3/4}
  #service  .c2{left:-9%;top:34%;width:54%;aspect-ratio:3/2}
  #service  .c3{left:44%;top:50%;width:48%;aspect-ratio:3/4}
  #service  .c4{left:3%;top:62%;width:40%;aspect-ratio:4/5}
  #service  .c5{left:50%;top:71%;width:56%;aspect-ratio:3/2}
  #service  .cg-copy{left:6%;top:84%;width:88%}
}


/* ============================================================================
   HERO FILM — a crossfading stack of stills reads as film without any video.
   Layers are real <img> in the markup so srcset, LQIP and no-JS all behave;
   this only moves the .on class. Alternating ken-burns directions stop the
   sequence feeling like a slideshow.
   ============================================================================ */
.hero-bg[data-hero-seq]{position:absolute;inset:0;overflow:hidden}
.hero-bg[data-hero-seq] img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  opacity:0;transition:opacity 2.4s var(--ease-io)}
.hero-bg[data-hero-seq] img.on{opacity:1}
.hero-bg[data-hero-seq] img:nth-child(odd).on{animation:kb 26s ease-in-out both}
.hero-bg[data-hero-seq] img:nth-child(even).on{animation:kbAlt 26s ease-in-out both}
@keyframes kbAlt{from{transform:scale(1.14) translate3d(1.5%,1%,0)}to{transform:scale(1.02) translate3d(-1%,-.5%,0)}}

/* beat one: the picture alone, held a moment darker and closer */
.hero-bg[data-hero-seq]{filter:brightness(.72) contrast(1.06);transition:filter 2.2s var(--ease-io) .35s}
body.ready .hero-bg[data-hero-seq]{filter:none}

@media (prefers-reduced-motion:reduce){
  .hero-bg[data-hero-seq] img{transition:none}
  .hero-bg[data-hero-seq] img.on{animation:none}
  .hero-bg[data-hero-seq]{filter:none;transition:none}
  .hero .vgrid::after{display:none}
}
html:not(.js) .hero-bg[data-hero-seq] img{opacity:0}
html:not(.js) .hero-bg[data-hero-seq] img:first-child{opacity:1}

/* ============================================================================
   PAGE TRANSITION — an ink curtain, so moving between pages is a cut in one
   film rather than a browser reload. Covers on the way out, lifts on arrival.
   ============================================================================ */
#curtain{position:fixed;inset:0;z-index:300;background:var(--ink,#16170F);
  transform:translateY(100%);pointer-events:none;will-change:transform}
#curtain.in{transform:translateY(0);transition:transform .52s var(--ease-io)}
#curtain.out{transform:translateY(-100%);transition:transform .72s var(--ease-io) .06s}
#curtain .mark{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  opacity:0;transition:opacity .3s ease}
#curtain.in .mark{opacity:.9;transition:opacity .34s ease .16s}
@media (prefers-reduced-motion:reduce){#curtain{display:none}}


/* ============================================================================
   VELOCITY — the picture inside a frame lags against a fast scroll and settles
   when you stop. The frame is already overflow:hidden, so the only thing this
   needs is a little headroom: scale(1.045) is imperceptible on a cover-fitted
   image and gives ±10px to move inside. This is the single change that makes
   scrolling feel physical rather than merely animated.
   ============================================================================ */
.media.mv img.loaded{transform:translate3d(0,var(--vel-y,0px),0) scale(1.045)}
/* the ring stretches into the direction of travel */
#cur-ring{transition:width .4s var(--ease-out),height .4s var(--ease-out)}
html.cursor-live #cur-ring{scale:1 calc(1 + var(--vel,0) * .18)}

/* ============================================================================
   CONTINUOUS LIFE — [data-life] blocks are driven every frame by motion.js.
   No transition here on purpose: the value IS the animation, and a transition
   would fight the per-frame write and lag behind the scroll.
   ============================================================================ */
[data-life]{will-change:transform,opacity}

/* ============================================================================
   CASCADE — [data-seq] children resolve from their own scroll position.
   The resting state is set here so a no-JS visitor sees the finished list.
   ============================================================================ */
html:not(.js) [data-seq]>*{opacity:1;transform:none}

@media (prefers-reduced-motion:reduce){
  .media.mv img.loaded{transform:none}
  html.cursor-live #cur-ring{scale:1}
  [data-life]{will-change:auto;transform:none!important;opacity:1!important}
}






