/* ALTIS — "Explore the smart home".
   Palette LOCKED (§6.5). Typography deliberately placeholder: one neutral sans.

   SCROLL CONTRACT (tests/t10-scroll-block.mjs): the page IS a scroll track.
   #scrolltrack gives the document real height, #stage is fixed over it, and
   window.scrollY drives every spot, dissolve and frame index continuously.
   Nothing here locks the y axis of html / body, there is no
   overscroll-behavior:none, and there is no touch-action declaration outside a
   control widget — the native scroller is never interfered with, because it is
   the entire mechanism. */

/* Scoped to #smart-home-experience (site integration): this file was authored
   for a standalone page and used :root / body / a global reset. Embedded as a
   section inside a longer page, all of that is scoped to the section root so
   it cannot leak into the surrounding site theme, and nothing from the site
   theme can leak in either. #stage is also switched from fixed to sticky here
   so it pins for the length of its own section instead of the whole document. */
#smart-home-experience {
  --altis-white:   #F7F8F5;
  --altis-mineral: #DCE3E8;
  --altis-slate:   #12202A;
  --altis-blue:    #0C63E7;
  --altis-amber:   #FF9D2E;
  --altis-slate-900: #0B141B;
  --altis-slate-800: #12202A;
  --altis-slate-700: #1A2B37;
  --altis-line:    rgba(247, 248, 245, 0.14);
  --altis-text-dim:#8B99A4;
  --altis-ease:    cubic-bezier(0.32, 0.72, 0.24, 1);
  /* The settle curve the spot transition uses (src/spotshow.js EASE_SETTLE).
     Everything that moves alongside a spot change borrows it, so no companion
     animation starts or stops at a rate the picture itself never does. */
  --altis-settle:  cubic-bezier(0.40, 0.03, 0.28, 1);

  /* live room-state vars, written by src/room.js each frame */
  --tier2: 1;
  --grainAmp: 1;
  --privacy: 1;
  --privacySoft: 1;
  --shading: 1;
  --phNight: 0;
  --phColour: 0;
  --phShade: 0;
  --phPrivOp: 1;
  --phShadeOp: 1;

  /* The graduated ND filter's strength, written by src/spotshow.js from the
     scroll offset so it crossfades with the picture instead of stepping. */
  --scrim: 1;

  position: relative;
  display: grid;
  /* Explicit, because the single implicit column would otherwise be an `auto`
     track sized from #scrolltrack's declared `width: 1px`. That stretches to
     the container in the normal case, but collapses to 1px the moment the
     container has no width to give, taking #stage (and every control measured
     against it) down with it. */
  grid-template-columns: minmax(0, 1fr);
  margin: 0;
  padding: 0;
  background: var(--altis-slate-900);
  font-family: var(--font-sans, "Archivo", "Helvetica Neue", Arial, sans-serif); /* resolved against the site's brand pairing on embed */
  color: var(--altis-mineral);
  /* No overflow-x here: setting only one axis forces the other to `auto`
     per the CSS Overflow spec, which would make this element its own
     scroll container and break position: sticky on #stage below. #stage
     already clips its own children (see its own `overflow: hidden`). */
}
#smart-home-experience, #smart-home-experience * { box-sizing: border-box; }

/* ================================================= the scroll track =========
   The element that makes the document tall. It is the ONLY thing on the page
   that occupies layout height: everything the visitor sees is painted on
   #stage, which is fixed and never moves. src/scrollworld.js overwrites this
   height in px from the per-spot bands in src/spots.js — 13.2 viewports of
   travel plus the one the visitor is looking through, so 1420vh is both the
   no-JS fallback and the honest declared size of the world.

   Nothing is drawn here and nothing is clickable here; it exists so that a
   wheel, a finger, the spacebar, Page Down and the scrollbar all have
   somewhere real to go. */
#scrolltrack {
  position: relative;
  z-index: 0;
  width: 1px;
  height: 1420vh;
  pointer-events: none;
  /* Overlaps #stage in the same grid cell: this element's height sets the
     section's scroll length, #stage pins across it via `position: sticky`. */
  grid-column: 1;
  grid-row: 1;
}

/* The stage is pinned. The scroll offset moves what is drawn on it, never it.

   It is held back until the landing spot and its neighbour have decoded
   (`body.ready`, src/main.js). render() runs from the first animation frame, so
   without this the first paint would be an empty layer with its src still in
   flight — the move used to await the decode before it dissolved, and this is
   where that guarantee went. src/main.js races the decode against 2.5s, so the
   page can never be left dark by a stalled asset. */
#stage {
  position: sticky;
  top: 0;
  left: 0;
  right: 0;
  bottom: auto;
  /* 100vh on iOS Safari is the LARGE viewport, measured with the URL bar
     hidden, so a stage sized in vh is taller than the part of the page you
     can actually see and the switcher and the hint along its bottom edge sit
     behind the browser's own toolbar. svh is the small viewport: never taller
     than what is on screen, and stable rather than resizing mid-scroll the
     way dvh does. vh stays first as the fallback. */
  height: 100vh;
  height: 100svh;
  grid-column: 1;
  grid-row: 1;
  overflow: hidden;
  isolation: isolate;
  z-index: 1;
  opacity: 0;
  transition: opacity 320ms ease;
}
#smart-home-experience.she-ready #stage { opacity: 1; }
#smart-home-experience.she-snap #stage { transition: none; }

/* =========================================================== spot layers ===
   One layer per registry entry. Both kinds fill the frame with `slice`
   semantics so a scene coordinate lands on the same pixel in a photo spot and
   in the composited residence. Only opacity and transform ever animate. */

#spots { position: absolute; inset: 0; }

.spot {
  position: absolute;
  inset: 0;
  opacity: 0;
  visibility: hidden;
  transform: scale(1);
  will-change: opacity, transform;
  backface-visibility: hidden;
}
.spot-inner {
  position: absolute;
  inset: 0;
  transform-origin: 50% 50%;
  will-change: transform;
}
.spot-img,
.spot-state {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;      /* === preserveAspectRatio="xMidYMid slice" */
  object-position: 50% 50%;
  display: block;
}
.spot-state { opacity: 0; }

/* The scrubbed frame sequence. It has to lay out exactly like .spot-img,
   because the cover geometry is computed in JS from clientWidth/clientHeight —
   without a box to measure, a canvas reports its default 300x150 and the
   sequence is drawn to the wrong scale (or not at all). It fades in over the
   poster, which is frame 0 of the same sequence, so there is never a blank. */
.spot-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  opacity: 0;
  transition: opacity 220ms ease;
}
.spot-canvas.is-ready { opacity: 1; }
#smart-home-experience.she-snap .spot-canvas { transition: none; }

/* A moving spot. The poster still (.spot-img) is frame 0 of this clip and sits
   underneath it, so the frame is filled from the first paint and the video
   only ever fades in over a picture of itself. Never built at all under
   prefers-reduced-motion or ?snap=1 (src/spotshow.js). */
.spot-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;      /* === preserveAspectRatio="xMidYMid slice" */
  object-position: 50% 50%;
  display: block;
  opacity: 0;
  transition: opacity 0.9s cubic-bezier(0.32, 0.10, 0.62, 0.92);
  pointer-events: none;
}
/* `is-ready` — not `is-playing`. The rooftop clip never plays: it is revealed
   the moment a frame has actually been decoded AND presented at the position
   the visitor is standing at, so the poster is never swapped in front of them.
   A looping clip adds the same class from its own `playing` event. */
.spot-video.is-ready { opacity: 1; }

/* The residence composite (src/scene/a03.js) lives inside its spot layer. */
.anchor { position: absolute; inset: 0; }
.anchor .scene { width: 100%; height: 100%; display: block; }

/* Procedural grain must be luminance-neutral: overlay blend keeps texture
   without adding a grey film over the material (F3 instrumentation). */
.grain { mix-blend-mode: overlay; }
.masks-outline { display: none; }

/* Production photo layer: hidden until the master is confirmed on disk. */
.anchor:not(.photo) #a03-photo { display: none; }
.anchor.photo .grain { display: none; }
.anchor:not(.photo) #a03-photo-t2 { display: none; }
.anchor.photo #a03-t2 { display: none; }
/* Over a photoreal master, Tier-1 only dims globally; placeholder accents off. */
.anchor.photo #a03-t1 .t1 > :not(:first-child) { display: none; }
.anchor.photo #a03-base > :not(:first-child) { display: none; }
/* The master already carries real GI, so COLOURBASE needs less dimming. */
.anchor.photo .t1[data-state="COLOURBASE"] > rect:first-child { opacity: 0.5; }
.anchor.photo #a03-photo-t2 .t2 { mix-blend-mode: screen; }
/* Rendered state stills replace their vector fallbacks when present. */
.anchor.has-SHADE_DOWN .shadefallback { display: none; }
.anchor.has-PRIVACY .privacyfallback { display: none; }

/* =============================================================== hotspots ===
   A spot's own fixtures, in the control surface's language: a warm core in a
   slow halo, a hairline ring and a label. The room is an "explore"
   destination, so an interactive point announces itself rather than hiding —
   hover and focus only intensify what is already legible. */

.spot-hotspots { position: absolute; inset: 0; pointer-events: none; }

.spot-hotspot {
  position: absolute;
  margin: 0; padding: 0;
  min-width: 44px; min-height: 44px;
  background: none;
  border: 0;
  border-radius: 6px;
  cursor: pointer;
  pointer-events: auto;
  -webkit-tap-highlight-color: transparent;
}
.spot-hotspot:focus-visible {
  outline: 2px solid #6BB0FF;
  outline-offset: 4px;
}
.sh-dot { position: absolute; inset: 0; display: block; }
/* A soft dark scrim keeps a 3px warm core readable against bright travertine. */
.sh-dot::before {
  content: "";
  position: absolute; left: 50%; top: 50%;
  width: 9px; height: 9px;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, rgba(6,12,17,0.42) 0%, rgba(6,12,17,0.22) 55%, rgba(6,12,17,0) 72%);
  box-shadow: 0 0 10px 5px rgba(6, 12, 17, 0.2);
}
.sh-core, .sh-halo {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  display: block;
}
.sh-core {
  width: 10px; height: 10px;
  background: #FFFDF8;
  box-shadow:
    0 0 5px 1.5px rgba(255, 226, 184, 1),
    0 0 16px 4px rgba(255, 186, 110, 0.8),
    0 0 32px 10px rgba(255, 168, 84, 0.38);
  transition: width 0.3s var(--altis-ease), height 0.3s var(--altis-ease), background 0.4s ease, box-shadow 0.4s ease;
}
.sh-halo {
  width: 54px; height: 54px;
  background: radial-gradient(circle, rgba(255,190,124,0.52) 0%, rgba(255,190,124,0.18) 46%, rgba(255,190,124,0) 70%);
  animation: sh-breathe 3s ease-in-out infinite;
}
@keyframes sh-breathe {
  0%, 100% { transform: translate(-50%, -50%) scale(0.8); opacity: 0.5; }
  50%      { transform: translate(-50%, -50%) scale(1.32); opacity: 1; }
}
/* Always-on ring: the target reads as a control at a glance. */
.spot-hotspot::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 44px; height: 44px;
  margin: -22px 0 0 -22px;
  border: 2px solid rgba(255, 220, 172, 0.78);
  border-radius: 50%;
  box-shadow: 0 0 0 1px rgba(6, 12, 17, 0.35), inset 0 0 12px rgba(255, 190, 124, 0.15);
  transition: border-color 0.3s ease, transform 0.3s var(--altis-ease), box-shadow 0.3s ease;
}
.spot-hotspot:hover::after,
.spot-hotspot:focus-visible::after {
  border-color: rgba(255, 226, 184, 0.95);
  transform: scale(1.14);
  box-shadow: 0 0 0 1px rgba(6, 12, 17, 0.5), 0 0 18px 3px rgba(255, 176, 92, 0.35);
}
.spot-hotspot:hover .sh-core,
.spot-hotspot:focus-visible .sh-core { width: 9px; height: 9px; }

/* Once the reader has authorised, the pip carries the blue the pillar carries. */
.spot-hotspot[aria-pressed="true"] .sh-core {
  background: #DCEBFF;
  box-shadow:
    0 0 4px 1px rgba(150, 200, 255, 1),
    0 0 14px 4px rgba(60, 140, 255, 0.75),
    0 0 30px 10px rgba(12, 99, 231, 0.35);
}
.spot-hotspot[aria-pressed="true"] .sh-halo {
  background: radial-gradient(circle, rgba(110,175,255,0.42) 0%, rgba(110,175,255,0.13) 46%, rgba(110,175,255,0) 70%);
  animation: none;
  opacity: 0.7;
  transform: translate(-50%, -50%) scale(1);
}
.spot-hotspot[aria-pressed="true"]::after {
  border-color: rgba(150, 200, 255, 0.6);
  box-shadow: 0 0 0 1px rgba(6, 12, 17, 0.4), inset 0 0 12px rgba(110, 175, 255, 0.18);
}

.sh-chip {
  position: absolute;
  left: calc(100% + 14px); top: 50%;
  transform: translateY(-50%);
  padding: 5px 11px;
  white-space: nowrap;
  font-size: 10.5px; font-weight: 500;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: rgba(255, 240, 222, 0.92);
  background: rgba(8, 14, 20, 0.72);
  border: 1px solid rgba(255, 214, 160, 0.28);
  border-radius: 3px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  pointer-events: none;
  transition: color 0.3s ease, border-color 0.3s ease;
}
.spot-hotspot:hover .sh-chip,
.spot-hotspot:focus-visible .sh-chip { color: #fff; border-color: rgba(255, 226, 184, 0.7); }
.spot-hotspot[aria-pressed="true"] .sh-chip {
  color: rgba(226, 240, 255, 0.95);
  border-color: rgba(150, 200, 255, 0.4);
}

/* ================================================================= scrim ===
   A graduated ND filter, not a UI panel: it darkens the top edge and the
   bottom-left corner just enough to hold white type over wet stone or a
   bright travertine pier, and disappears across the rest of the frame. */

#stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 15;
  pointer-events: none;
  background:
    radial-gradient(112% 82% at 0% 100%,
      rgba(6, 12, 17, 0.72) 0%, rgba(6, 12, 17, 0.36) 28%, rgba(6, 12, 17, 0) 58%),
    /* The copy column. A terrace at golden hour is brighter than a room at
       dusk, and one graduated edge has to hold white type over both. */
    linear-gradient(to right,
      rgba(6, 12, 17, 0.46) 0%, rgba(6, 12, 17, 0.22) 16%, rgba(6, 12, 17, 0.06) 32%, rgba(6, 12, 17, 0) 44%),
    linear-gradient(to bottom,
      rgba(6, 12, 17, 0.42) 0%, rgba(6, 12, 17, 0.12) 11%, rgba(6, 12, 17, 0) 22%);
}
/* The tech wall paints its own ground and its own captions; a second ND filter
   over it only greys the tiles — so the scrim lifts for it. That used to be a
   class swap with a 1.15s transition chasing a 1.75s dissolve, which is a
   visible step in the exposure halfway through the move. It is now a scroll-
   derived number written by render() in src/spotshow.js, blended across the
   transition exactly like the picture it sits on, and therefore never late. */
#stage::before {
  opacity: var(--scrim, 1);
  transition: none;
}

/* =================================================================== copy ===
   One title and one line per spot, from the registry. Never two. */

#copy {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 20;
}
/* The copy is NOT part of the move. It leaves first and unmourned, and it
   arrives late — a third of the way into the dissolve, after the picture has
   started settling — so a place is established before it is captioned. This
   asymmetry is what stops a title sliding around like a lower third, and it is
   also what stops two different sentences dissolving through each other, which
   is a smudge rather than a fade.

   Opacity, transform and visibility are written here by render() in
   src/spotshow.js on every animation frame, from the scroll offset (see
   COPY_OUT_END / COPY_IN_START there — the same 30%-out / 34%-late shape the
   old 0.52s-out / 0.56s-delayed CSS transitions had). The rest below is the
   position and the type; JS never touches those. */
.spotcopy {
  position: absolute;
  left: clamp(24px, 6vw, 92px);
  top: 26vh;
  max-width: min(520px, 46vw);
  opacity: 0;
  visibility: hidden;
  transform: translateY(16px);
  text-shadow: 0 2px 30px rgba(6, 12, 17, 0.55), 0 1px 4px rgba(6, 12, 17, 0.4);
}

/* ================================================================== CTA ===
   One action, on the last spot only. Quiet: a hairline and a word, the same
   language as the switcher, sized as a real target. */

.spotcta { margin: 30px 0 0; }
.spotcopy .cta {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  padding: 0 26px;
  pointer-events: auto;          /* #copy is inert; this one thing is not */
  color: var(--altis-white);
  text-decoration: none;
  font-size: 11px; font-weight: 600;
  letter-spacing: 0.24em; text-transform: uppercase;
  background: rgba(8, 14, 20, 0.42);
  border: 1px solid rgba(247, 248, 245, 0.42);
  border-radius: 2px;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  text-shadow: none;
  transition: background-color 0.4s ease, border-color 0.4s ease, color 0.4s ease;
}
.spotcopy .cta:hover {
  background: var(--altis-white);
  border-color: var(--altis-white);
  color: var(--altis-slate-900);
}
.spotcopy .cta:focus-visible {
  outline: 2px solid #6BB0FF;
  outline-offset: 4px;
}
.spotcopy h1 {
  font-size: clamp(28px, 3.4vw, 48px);
  font-weight: 620; line-height: 1.08;
  letter-spacing: -0.015em;
  color: var(--altis-white);
  margin: 0 0 16px;
}
.spotcopy .support {
  font-size: clamp(14px, 1.15vw, 17px);
  line-height: 1.6;
  color: var(--altis-mineral);
  max-width: 40ch;
  margin: 0;
}

/* ========================================================= technology ======
   The one spot that is not a photographed position: four photographic tiles
   over a dark ground. The chrome is a hairline, a scrim and a caption —
   nothing else, because the pictures are still the product.

   Layout: an editorial left column (heading, and the switcher below it) and a
   2×2 wall to its right. On mobile the wall becomes one column under the
   heading. Nothing here scrolls: the grid is sized to the frame it is in. */

.spot--tech { background: var(--altis-slate-900); }
.spot--tech .spot-inner {
  background:
    radial-gradient(120% 90% at 78% 8%, rgba(30, 52, 66, 0.55) 0%, rgba(11, 20, 27, 0) 62%),
    linear-gradient(160deg, #101E27 0%, #0B141B 58%, #080F14 100%);
}

.tech {
  position: absolute;
  inset: 0;
  padding: clamp(92px, 12vh, 132px) clamp(24px, 6vw, 92px) clamp(26px, 5vh, 54px);
  display: grid;
  grid-template-columns: minmax(190px, 300px) minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr);
  column-gap: clamp(28px, 4vw, 64px);
  align-items: stretch;
}
.tech-head {
  align-self: start;
  max-width: 340px;
  opacity: 0;
  transform: translateY(14px);
  /* Late, like every other spot's copy — but inside the transition, not after
     it: a wall that is still empty when the dissolve finishes is a black hole. */
  transition:
    opacity 0.85s cubic-bezier(0.32, 0.10, 0.62, 0.92) 0.28s,
    transform 1.05s var(--altis-settle) 0.28s;
}
.tech.is-live .tech-head { opacity: 1; transform: translateY(0); }
.tech-head h1 {
  font-size: clamp(26px, 2.9vw, 42px);
  font-weight: 620; line-height: 1.08;
  letter-spacing: -0.015em;
  color: var(--altis-white);
  margin: 0 0 16px;
}
.tech-head .support {
  font-size: clamp(13px, 1.05vw, 16px);
  line-height: 1.6;
  color: var(--altis-mineral);
  max-width: 34ch;
  margin: 0;
}

.tech-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: clamp(9px, 1.1vw, 16px);
  min-height: 0;
  min-width: 0;
}

/* ---- a box ------------------------------------------------------------- */

.tbox {
  position: relative;
  min-width: 0; min-height: 0;
  margin: 0; padding: 0;
  border: 0;
  border-radius: 3px;
  background: #0C161D;
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  font: inherit;
  color: inherit;
  -webkit-tap-highlight-color: transparent;
  box-shadow: inset 0 0 0 1px rgba(247, 248, 245, 0.10);
  transition: box-shadow 0.45s ease;
  /* A soft stagger as the wall arrives. Deliberately NOT a fade from zero:
     the incoming layer sits under the outgoing one at full opacity, so a wall
     that is still transparent while the last place leaves puts a hole in the
     middle of the transition. The tiles are there from the first frame — they
     only settle. */
  transform: translateY(18px);
}
.tech.is-live .tbox {
  transform: translateY(0);
  /* The stagger is stretched to sit inside the longer move: the last tile
     lands at ~1.3s, just after the dissolve, so the wall finishes settling
     rather than snapping into place while the previous place is still fading. */
  transition:
    transform 1s var(--altis-settle) calc(0.10s + var(--i, 0) * 0.07s),
    box-shadow 0.45s ease;
}
.tbox-img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  display: block;
  z-index: 0;
  filter: brightness(0.82) saturate(0.96);
  transform: scale(1.001);
  transition: transform 1.1s var(--altis-ease), filter 0.5s ease;
}
/* The caption ground: a graduated ND filter over the lower third, not a bar. */
.tbox::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(6, 12, 17, 0.86) 0%, rgba(6, 12, 17, 0.52) 22%, rgba(6, 12, 17, 0.06) 52%, rgba(6, 12, 17, 0) 70%);
}
.tbox-cap {
  position: absolute;
  z-index: 2;
  left: clamp(14px, 1.5vw, 22px);
  right: clamp(14px, 1.5vw, 22px);
  bottom: clamp(13px, 1.4vw, 20px);
  pointer-events: none;
}
.tbox-title {
  display: block;
  font-size: clamp(15px, 1.35vw, 20px);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--altis-white);
  text-shadow: 0 1px 18px rgba(6, 12, 17, 0.8);
}
.tbox-line {
  display: block;
  margin-top: 6px;
  max-width: 46ch;
  font-size: clamp(11.5px, 0.85vw, 13.5px);
  line-height: 1.5;
  color: rgba(220, 227, 232, 0.82);
  text-shadow: 0 1px 14px rgba(6, 12, 17, 0.9);
  transition: color 0.4s ease;
}
.tbox:hover, .tbox:focus-visible { box-shadow: inset 0 0 0 1px rgba(255, 214, 160, 0.52); }
.tbox:hover .tbox-img,
.tbox:focus-visible .tbox-img { transform: scale(1.055); filter: brightness(1) saturate(1); }
.tbox:hover .tbox-line, .tbox:focus-visible .tbox-line { color: var(--altis-white); }
.tbox:focus-visible { outline: 2px solid #6BB0FF; outline-offset: 3px; }

/* ---- box four: one box, three amenities -------------------------------- */
/* Three equal horizontal slices. A slice lifts and brightens under the
   pointer or the focus ring, and opens its own full frame. */

.tbox-trip {
  display: grid;
  grid-template-rows: repeat(3, minmax(0, 1fr));
  gap: 2px;
  background: transparent;
  overflow: visible;
  box-shadow: none;
  cursor: default;
}
/* The triptych's caption ground is a corner, not a band: a full-width scrim
   here would put the café slice behind a curtain to caption the whole box. */
.tbox-trip::after {
  border-radius: 3px;
  background: radial-gradient(86% 62% at 0% 100%,
    rgba(6, 12, 17, 0.92) 0%, rgba(6, 12, 17, 0.58) 34%, rgba(6, 12, 17, 0.18) 58%, rgba(6, 12, 17, 0) 76%);
}
.tslice {
  position: relative;
  min-height: 0; min-width: 0;
  margin: 0; padding: 0;
  border: 0;
  border-radius: 2px;
  background: #0C161D;
  overflow: hidden;
  cursor: pointer;
  font: inherit; color: inherit;
  -webkit-tap-highlight-color: transparent;
  box-shadow: inset 0 0 0 1px rgba(247, 248, 245, 0.10);
  transform: translateY(0);
  transition: transform 0.42s var(--altis-ease), box-shadow 0.42s ease;
}
.tslice-img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  display: block;
  filter: brightness(0.74) saturate(0.94);
  transform: scale(1.001);
  transition: transform 0.9s var(--altis-ease), filter 0.42s ease;
}
.tslice-label {
  position: absolute;
  z-index: 2;
  right: clamp(12px, 1.2vw, 18px);
  top: 50%;
  transform: translateY(-50%);
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
  color: rgba(255, 240, 222, 0.86);
  text-shadow: 0 1px 12px rgba(6, 12, 17, 0.95);
  pointer-events: none;
  transition: color 0.4s ease;
}
.tslice:hover, .tslice:focus-visible {
  transform: translateY(-4px);
  z-index: 3;
  box-shadow: inset 0 0 0 1px rgba(255, 214, 160, 0.6), 0 8px 22px rgba(0, 0, 0, 0.5);
}
.tslice:hover .tslice-img,
.tslice:focus-visible .tslice-img { filter: brightness(1.06) saturate(1); transform: scale(1.05); }
.tslice:hover .tslice-label, .tslice:focus-visible .tslice-label { color: #fff; }
.tslice:focus-visible { outline: 2px solid #6BB0FF; outline-offset: 2px; }
/* The triptych's own caption rides over the slices without eating their clicks. */
.tbox-trip .tbox-cap { z-index: 4; }

/* ---- an opened box ------------------------------------------------------ */
/* Full-bleed, over its own ground, above the switcher. It grows out of the
   tile that was clicked (transform-origin set in src/tech.js) so the picture
   the visitor pointed at is the one that expands. */

.tech-open {
  position: absolute;
  inset: 0;
  z-index: 90;
  background: var(--altis-slate-900);
  opacity: 0;
  transform: scale(0.92);
  transition: opacity 450ms ease, transform 450ms var(--altis-ease);
  will-change: opacity, transform;
}
.tech-open[hidden] { display: none; }
.tech-open.is-open { opacity: 1; transform: scale(1); }
.to-img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  display: block;
  cursor: zoom-out;
}
.tech-open::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(105% 78% at 0% 100%,
      rgba(6, 12, 17, 0.78) 0%, rgba(6, 12, 17, 0.34) 30%, rgba(6, 12, 17, 0) 60%),
    linear-gradient(to bottom, rgba(6, 12, 17, 0.34) 0%, rgba(6, 12, 17, 0) 20%);
}
.to-copy {
  position: absolute;
  z-index: 2;
  left: clamp(24px, 6vw, 92px);
  bottom: clamp(34px, 7vh, 74px);
  max-width: min(560px, 74vw);
  pointer-events: none;
  text-shadow: 0 2px 30px rgba(6, 12, 17, 0.6), 0 1px 4px rgba(6, 12, 17, 0.5);
}
.to-copy h2 {
  margin: 0 0 12px;
  font-size: clamp(24px, 3vw, 40px);
  font-weight: 620; line-height: 1.1; letter-spacing: -0.015em;
  color: var(--altis-white);
}
.to-copy p {
  margin: 0;
  font-size: clamp(13px, 1.1vw, 16px);
  line-height: 1.6;
  color: var(--altis-mineral);
  max-width: 42ch;
}
.to-close {
  position: absolute;
  z-index: 3;
  top: clamp(20px, 3.4vh, 34px);
  right: clamp(20px, 3vw, 44px);
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 46px;
  padding: 0 18px;
  margin: 0;
  border: 1px solid rgba(247, 248, 245, 0.34);
  border-radius: 2px;
  background: rgba(8, 14, 20, 0.55);
  color: var(--altis-white);
  font: inherit;
  cursor: pointer;
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  -webkit-tap-highlight-color: transparent;
  transition: background-color 0.35s ease, border-color 0.35s ease, color 0.35s ease;
}
.to-close:hover { background: var(--altis-white); border-color: var(--altis-white); color: var(--altis-slate-900); }
.to-close:focus-visible { outline: 2px solid #6BB0FF; outline-offset: 4px; }
.to-closeword {
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.22em; text-transform: uppercase;
}
.to-x { position: relative; display: block; width: 13px; height: 13px; }
.to-x::before, .to-x::after {
  content: "";
  position: absolute;
  left: 0; top: 50%;
  width: 100%; height: 1.5px;
  background: currentColor;
}
.to-x::before { transform: rotate(45deg); }
.to-x::after  { transform: rotate(-45deg); }

/* ============================================================== wordmark ===*/

#wordmark {
  position: absolute;
  /* Back at the designed 28px: the site header veils itself while this
     section is pinned (.nav-veiled in src/css/site.css), so the top-left
     corner is the experience's own again. */
  top: 28px;
  left: clamp(24px, 6vw, 92px);
  z-index: 70;
  font-size: 15px; font-weight: 600; letter-spacing: 0.34em;
  color: var(--altis-white);
  text-shadow: 0 1px 20px rgba(6, 12, 17, 0.6);
}
#wordmark .protolabel {
  display: block;
  margin-top: 6px;
  font-size: 10px; font-weight: 400; letter-spacing: 0.12em;
  color: rgba(220, 227, 232, 0.58);
  text-transform: uppercase;
  text-shadow: 0 1px 12px rgba(6, 12, 17, 0.9);
}

/* ============================================================== switcher ===
   Not a nav bar. An index in the margin of a drawing: a number, a rule, a
   name. The rule is the whole interface — it extends to mark where you are. */

#spotnav {
  position: absolute;
  left: clamp(24px, 6vw, 92px);
  bottom: clamp(26px, 5vh, 54px);
  z-index: 70;
}
.sn-list {
  list-style: none;
  margin: 0; padding: 0;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.sn-btn {
  display: flex;
  align-items: center;
  gap: 14px;
  min-height: 44px;
  padding: 0 10px 0 0;
  margin: 0;
  background: none;
  border: 0;
  cursor: pointer;
  color: rgba(220, 227, 232, 0.66);
  font: inherit;
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.sn-num {
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.16em;
  color: rgba(220, 227, 232, 0.5);
  text-shadow: 0 1px 14px rgba(6, 12, 17, 0.85);
  transition: color 0.55s ease;
}
/* THE RULE IS THE INTERFACE, and it is now scroll-driven like everything else.
   `--w` is this place's share of the frame, written every animation frame by
   render() in src/spotshow.js: 1 while you are standing in it, falling to 0
   across the dissolve out of it. So the rule extends and retreats exactly as
   the picture does, instead of snapping when a spot "becomes" current and then
   easing for a second afterwards. Only the colour is still a transition —
   colour is the one thing that has no honest intermediate value here. */
.sn-rule {
  display: block;
  width: calc(16px + var(--w, 0) * 30px);
  height: 1px;
  background: currentColor;
  opacity: calc(0.42 + var(--w, 0) * 0.58);
  transition: background-color 0.35s ease;
}
.sn-label {
  font-size: 11px; font-weight: 500;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  white-space: nowrap;
  transition: color 0.55s ease, opacity 0.55s ease;
  text-shadow: 0 1px 3px rgba(6, 12, 17, 0.75), 0 1px 18px rgba(6, 12, 17, 0.9);
}
/* Hover adds to the rule rather than replacing it, so pointing at a place you
   are half-way into does not shorten the line. */
.sn-btn:hover .sn-rule {
  width: calc(26px + var(--w, 0) * 24px);
  opacity: calc(0.75 + var(--w, 0) * 0.25);
}
.sn-btn:hover .sn-label { color: var(--altis-white); }
.sn-btn:focus-visible {
  outline: 2px solid #6BB0FF;
  outline-offset: 4px;
  border-radius: 3px;
}
/* `is-current` is set the moment a place owns more than half the frame. Width
   and opacity are already continuous through `--w`, so all this carries is the
   colour — and the a11y state that goes with it (aria-current). */
.sn-btn.is-current { color: var(--altis-white); }
.sn-btn.is-current .sn-rule { background: var(--altis-amber); }
.sn-btn.is-current .sn-num { color: var(--altis-white); transition: color 0.5s ease; }
.sn-btn.is-current .sn-label { color: var(--altis-white); transition: color 0.5s ease; }

/* …and where the engine can mix colours, even the amber is continuous: the
   rule warms as its place takes the frame instead of switching at the halfway
   mark and then easing for the best part of a second after it. The block above
   is the fallback for engines without color-mix(). */
@supports (background: color-mix(in srgb, red 50%, blue)) {
  .sn-rule,
  .sn-btn.is-current .sn-rule {
    background: color-mix(in srgb, var(--altis-amber) calc(var(--w, 0) * 100%), currentColor);
    transition: none;
  }
}

/* ============================================================ move hint ===*/

#movehint {
  position: absolute;
  right: clamp(20px, 4vw, 44px);
  bottom: clamp(20px, 4vh, 34px);
  z-index: 70;
  margin: 0;
  max-width: 46vw;
  font-size: 10px; font-weight: 500;
  letter-spacing: 0.2em; text-transform: uppercase;
  color: rgba(220, 227, 232, 0.44);
  text-shadow: 0 1px 14px rgba(6, 12, 17, 0.8);
  pointer-events: none;
  opacity: 1;
  transition: opacity 0.9s ease;
}
#movehint.gone { opacity: 0; }
/* The wall reaches the right edge; the hint would sit on a photograph. */
#smart-home-experience[data-spot="TECH"] #movehint { opacity: 0; }

/* ============================================================== a11y ===*/

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

/* =========================================================== responsive ===*/

@media (max-width: 860px) {
  /* Five spots stacked at 44px each would eat 220px of a phone. The index
     lies down instead: every spot keeps its number and its 44px target, and
     only the one you are standing in spells its name out. Same idea — the
     rule extends to mark where you are — one axis over. */
  #spotnav { right: clamp(20px, 6vw, 92px); }
  .sn-list { flex-direction: row; align-items: center; flex-wrap: nowrap; }
  .sn-btn { gap: 9px; min-width: 44px; padding: 0 4px; justify-content: center; }
  .sn-btn .sn-rule, .sn-btn .sn-label { display: none; }
  .sn-btn.is-current .sn-rule { display: block; width: 20px; }
  .sn-btn.is-current .sn-label { display: block; }

  .spotcopy {
    top: auto;
    bottom: calc(clamp(26px, 5vh, 54px) + 74px);
    max-width: min(84vw, 460px);
  }
  #movehint { display: none; }

  /* The wall becomes one column under its heading. */
  .tech {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
    row-gap: clamp(14px, 2.4vh, 26px);
    padding: clamp(78px, 11vh, 108px) clamp(20px, 5vw, 48px) calc(clamp(26px, 5vh, 54px) + 62px);
  }
  .tech-head { max-width: none; }
  .tech-head h1 { font-size: clamp(22px, 5.6vw, 30px); margin-bottom: 10px; }
  .tech-head .support { font-size: 13px; max-width: 46ch; }
  .tech-grid {
    grid-template-columns: minmax(0, 1fr);
    /* the triptych earns extra height so each slice stays a real target */
    grid-template-rows: 1fr 1fr 1fr 1.55fr;
  }
  .tbox-line { font-size: 11.5px; max-width: 40ch; }
  .to-close { min-height: 44px; padding: 0 14px; }
}

@media (max-width: 620px) {
  #wordmark { top: 20px; letter-spacing: 0.28em; font-size: 13px; }
  .spotcopy { bottom: 100px; max-width: 86vw; }
  .spotcopy h1 { font-size: clamp(24px, 7vw, 32px); margin-bottom: 12px; }
  .spotcopy .support { font-size: 14px; }
  .spotcta { margin-top: 22px; }
  #spotnav { left: clamp(20px, 6vw, 92px); bottom: 22px; }
  .sn-btn { min-height: 44px; }
  .sn-label { font-size: 10px; letter-spacing: 0.2em; }
  .sh-chip { display: none; }
  .to-closeword { display: none; }
  .to-close { padding: 0 15px; }
}

/* Reduced motion reduces MOTION, not function. What goes: the lateral drift
   across the dissolve, the copy's rise, the scroll smoothing (src/scrollworld.js)
   and everything that breathes on its own. What emphatically stays: the frame
   sequence on the terrace, the push toward the doors, the crossfade and every
   band of the track. Those are direct manipulation — the visitor's own scroll,
   one-to-one — and a previous version that switched the whole scrub system off
   here shipped a section of dead stills. Handled in JS as well; this is the
   belt to that brace. */
@media (prefers-reduced-motion: reduce) {
  .sh-halo { animation: none; }
  .sn-num, .sn-label,
  .sn-btn.is-current .sn-num,
  .sn-btn.is-current .sn-label { transition-duration: 0.26s; }
  /* The wall arrives, it does not assemble itself. */
  .tech-head, .tech.is-live .tech-head { transition-duration: 0.2s; transition-delay: 0s; transform: none; }
  .tbox { transform: none; }
  .tech.is-live .tbox { transition: box-shadow 0.3s ease; transform: none; }
  .tbox-img, .tslice-img { transition-duration: 0.2s; }
  .tbox:hover .tbox-img, .tbox:focus-visible .tbox-img,
  .tslice:hover .tslice-img, .tslice:focus-visible .tslice-img { transform: scale(1.001); }
  .tslice:hover, .tslice:focus-visible { transform: none; }
  .tech-open { transition-duration: 0.18s; transform: none; }
  .tech-open.is-open { transform: none; }
}

/* ?snap=1 — deterministic captures: no easing, no animation, anywhere. */
#smart-home-experience.she-snap *,
#smart-home-experience.she-snap *::before,
#smart-home-experience.she-snap *::after {
  transition-duration: 0s !important;
  transition-delay: 0s !important;
  animation-duration: 0s !important;
  animation-delay: 0s !important;
  animation-iteration-count: 1 !important;
}

/* ---------- brand ----------
   Four placements, and no more: the wordmark here, the maker's badge on the
   control panel, the favicon, and the mark as a sign-off on the closing frame.
   Never over the photography — the imagery is the product. */
#wordmark { display: flex; align-items: baseline; gap: 12px; }
#wordmark img { height: 20px; width: auto; display: block; opacity: 0.96; }
@media (max-width: 620px) { #wordmark img { height: 16px; } }

.spot-mark {
  display: block;
  height: 30px; width: auto;
  margin: 0 0 22px;
  opacity: 0.9;
}
@media (max-width: 620px) { .spot-mark { height: 24px; margin-bottom: 16px; } }

/* The callback window. Masked to a single apartment's glazing, tinted with
   whatever the visitor left burning in the residence, screened over the
   photograph so it reads as light rather than paint. Invisible until they
   have actually taken the controls. */
.spot-callback {
  position: absolute;
  inset: 0;
  background: var(--cbColor, #ffce8f);
  mix-blend-mode: color;
  opacity: var(--cbOn, 0);
  transition: opacity 900ms ease, background-color 700ms ease;
  pointer-events: none;
  -webkit-mask-size: cover;
          mask-size: cover;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}
#smart-home-experience.she-snap .spot-callback { transition: none; }

/* The bloom: a whisper of the same colour screened over the glass so the
   window reads as lit rather than merely coloured. */
.spot-callback::after {
  content: "";
  position: absolute;
  inset: 0;
  background: inherit;
  mix-blend-mode: screen;
  opacity: 0.28;
}

/* Wordmark: the coloured mark carries the memory, white type carries the
   legibility over a dark photograph. */
#wordmark .wm-mark { height: 26px; width: auto; display: block; }
#wordmark .wm-name {
  font-size: 13px; font-weight: 600; letter-spacing: 0.22em;
  color: var(--altis-white, #F7F8F5);
}
@media (max-width: 620px) {
  #wordmark .wm-mark { height: 20px; }
  #wordmark .wm-name { font-size: 11px; letter-spacing: 0.16em; }
}


/* ===================================================== mobile: the room ===
   A 9:16 crop of the residence loses the partition, the intercom and the wall
   panel — two of its three controls. Every other spot crops well and keeps
   filling the screen; the residence alone switches to FIT, so the whole room
   (and every hotspot, mask and matched state) survives on a phone. The copy
   moves above it rather than over it. */
@media (max-aspect-ratio: 1/1) {
  .spot[data-spot="ROOM"] .spot-img,
  .spot[data-spot="ROOM"] .spot-state,
  .spot[data-spot="ROOM"] .scene {
    object-fit: contain;
  }
  .spot[data-spot="ROOM"] .spot-inner {
    top: 50%;
    bottom: auto;
    height: 56.25vw;          /* 16:9 of the full width */
    max-height: 100%;
    transform: translateY(-50%);
  }
  .spot[data-spot="ROOM"] .scene { height: 100%; }

  /* Copy sits above the room rather than across it, so nothing important is
     covered on a narrow screen. */
  #smart-home-experience[data-spot="ROOM"] .spotcopy {
    top: 12vh;
    bottom: auto;
    justify-content: flex-start;
  }
}
