/* ============================================================================
   NOBLE MOBILE — scenes
   ========================================================================= */

/* ═══════════════════════════════════════════════════ the field ══════════ */
/* Behind the whole page: a low-density drift of small coloured points moving
   on slow orbits, with two soft washes for depth. It is deliberately faint —
   a lit atmosphere, not a particle effect — and everything that matters sits
   above it with full contrast. See js/field.js. */
.field {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  overflow: hidden;
  opacity: 0;
  transition: opacity 1.6s var(--ease);
}
.field.is-lit { opacity: 1; }
.field__canvas { position: absolute; inset: 0; display: block; }

.field__wash {
  position: absolute;
  border-radius: 50%;
  filter: blur(80px);
}
.field__wash--a {
  width: 68vmax; height: 68vmax;
  left: -20vmax; top: -18vmax;
  background: radial-gradient(circle,
              rgba(232, 120, 70, .055) 0%,
              rgba(214, 88, 140, .020) 40%,
              transparent 70%);
}
.field__wash--b {
  width: 60vmax; height: 60vmax;
  right: -22vmax; bottom: -18vmax;
  background: radial-gradient(circle,
              rgba(70, 132, 214, .055) 0%,
              rgba(140, 96, 200, .020) 42%,
              transparent 72%);
}

/* ═══════════════════════════════════════════════════ 00 · ENTER ══════════ */
.enter {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  background: var(--void);
  transition: opacity 1.1s var(--ease), visibility 1.1s;
}
.enter.is-done { opacity: 0; visibility: hidden; pointer-events: none; }

/* a very faint field of light, far away */
.enter__field {
  position: absolute; inset: 0;
  background:
    radial-gradient(60% 45% at 50% 52%, rgba(138,103,18,.10), transparent 70%),
    radial-gradient(40% 30% at 50% 50%, rgba(22,22,28,.035), transparent 70%);
  opacity: 0;
}

.enter__stage {
  position: relative;
  width: min(62vw, 520px);
  aspect-ratio: 2442 / 1907;
}

/* the assembly board — pieces are placed in the artwork's own coordinate space */
.assembly { position: absolute; inset: 0; }
.assembly .piece {
  position: absolute;
  background-color: var(--brand-ink);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;   mask-size: 100% 100%;
  will-change: transform, opacity, filter;
}

/* the progress hairline under the assembly */
.enter__meter {
  position: absolute;
  bottom: clamp(40px, 9vh, 90px);
  width: min(30vw, 220px);
  height: 1px;
  background: var(--line);
  overflow: hidden;
}
.enter__meter i {
  display: block; height: 100%; width: 100%;
  background: var(--gold);
  transform: scaleX(0); transform-origin: left;
}


/* ═══════════════════════════════════════════════════ chrome ═════════════ */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 18px var(--gutter);
  opacity: 0;
  transform: translateY(-12px);
  pointer-events: none;
}
.nav.is-in { pointer-events: auto; }
/* Out of the way on the way down. The move itself is driven by GSAP (the
   entrance already owns this element's transform, and a competing CSS one would
   fight it) — this only stops it catching clicks while it is gone. */

.nav::before {
  content: "";
  position: absolute; inset: 0;
  backdrop-filter: blur(18px) saturate(140%);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  background: color-mix(in srgb, var(--void) 62%, transparent);
  border-bottom: 1px solid transparent;
  opacity: 0;
  transition: opacity .5s var(--ease), border-color .5s var(--ease);
  z-index: -1;
}
.nav.is-stuck::before { opacity: 1; border-bottom-color: var(--line); }

/* align-items: center keeps the two locked to a shared centre line, so the
   mark can grow without the wordmark shifting off it. */
.nav__brand { display: flex; align-items: center; gap: 12px; }
.nav__glyph { width: 44px; }        /* the mark only */
.nav__word  { width: 108px; }       /* unchanged */

/* the pill — the same shape the Dynamic Island scene hands over */
.nav__pill {
  display: flex; gap: 4px;
  padding: 5px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: color-mix(in srgb, var(--raise) 82%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
/* The pill is a pseudo-element rather than a background colour so it can grow
   into place instead of switching on. Nothing here is ever left highlighted:
   there used to be an is-here state that filled the current section's link with
   gold, and because two of these links point at the same section it lit two of
   them at once. The nav says where you can go, not where you are. */
.nav__pill a {
  position: relative;
  isolation: isolate;
  padding: 8px 15px;
  border-radius: 999px;
  font-size: .82rem;
  color: var(--text-2);
  white-space: nowrap;
  transition: color .35s var(--ease);
}
.nav__pill a::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  opacity: 0;
  scale: .84;
  transition: opacity .32s var(--ease), scale .32s var(--ease);
}
.nav__pill a:hover { color: var(--text); }
.nav__pill a:hover::before,
.nav__pill a:focus-visible::before { opacity: 1; scale: 1; }

.nav__end { display: flex; align-items: center; gap: 14px; }

.nav__cta {
  padding: 9px 20px;
  border-radius: 999px;
  font-size: .82rem;
  border: 1px solid var(--line-2);
  transition: background-color .4s var(--ease), color .4s var(--ease), border-color .4s var(--ease);
}
.nav__cta:hover { background: var(--gold); border-color: var(--gold); color: #fff; }

/* ------------------------------------------------------------- the iris --- */
/* The signature transition. A lens aperture opens out of the point you were
   looking at and the next layer is already inside it. */
.iris {
  position: fixed; inset: 0; z-index: 120;
  pointer-events: none;
  background: var(--void);
  clip-path: circle(0% at 50% 50%);
  opacity: 0;
}
.iris__ring {
  position: absolute; inset: 0;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px var(--gold-soft);
  opacity: 0;
}


/* ═══════════════════════════════════════════════════ scene shell ════════ */
.pinned { height: 100vh; }

.stage {
  position: relative;
  height: 100vh;
  overflow: hidden;
  display: grid;
  place-items: center;
}
.stage--flow { height: auto; min-height: 100vh; overflow: visible; padding-block: clamp(90px, 14vh, 180px); }

/* The cinematic surface: a slightly cooler plate than the page around it.

   It is a wash rather than a seal. At full opacity it covered the field on
   every scene but the hero, which is most of the site — the drift was running
   and nothing could see it. Held here it still reads as its own tone and the
   points move through it. */
.scene--cine .stage {
  background: color-mix(in srgb, var(--cine) 46%, transparent);

  /* This surface is black in both themes, so it re-points the text tokens at
     values that read on black. Anything placed inside a cinematic stage is then
     legible in light mode too, without having to be patched one rule at a time. */
  --text:   var(--cine-text);
  --text-2: var(--cine-text-2);
  --text-3: var(--cine-text-3);
  color: var(--text);
}
.scene--cine .stage { height: 100vh; }
.scene--cine .stage--flow { height: auto; }
.scene--cine .label--gold { color: var(--gold); }

.stage__bg {
  position: absolute; inset: 0;
  background: radial-gradient(70% 60% at 50% 55%, rgba(212,162,76,.055), transparent 72%);
}

/* the elements that reveal themselves as a scene arrives */
[data-r] { will-change: transform, opacity; }


/* ═══════════════════════════════════════════════════ 01 · ORIGIN ════════ */
.origin {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: center;
  /* no fill: this is already the page colour, and painting it again would
     bury the field on the one screen everybody sees */
  overflow: hidden;
}
/* ---- the Apple plate, as a backdrop ----------------------------------------
   The supplied image is lit on black. On a light page that would land as a grey
   smudge, so it is inverted to white-on-white, the hue rotation puts the gold
   back where the inversion sent it, and multiply lets the white fall away —
   leaving the same Apple faintly etched into the page rather than glowing out
   of the dark. It is also masked to fade out well inside its own rectangle, so
   the plate never shows an edge.                                             */
.origin__atmos {
  position: absolute;
  left: 50%; top: 50%;
  translate: -50% -50%;
  z-index: 0;
  /* Big enough that the Apple surrounds the lockup rather than sitting beside
     it — at this size the glyph is wider and taller than the Noble Mobile logo,
     so the logo reads as standing inside it. The plate is upscaled about 2x,
     which costs nothing on a layer that is blurred and held at 62%. */
  width: min(240vw, 3400px);
  pointer-events: none;
  mix-blend-mode: multiply;       /* the plate's whites drop out of the page */
  opacity: 0;                     /* the reveal brings it up */
  will-change: opacity, transform;
}
.origin__atmos__drift { width: 100%; height: 100%; will-change: opacity, transform; }
.origin__atmos img {
  width: 100%; height: auto;
  filter: invert(1) hue-rotate(180deg) saturate(108%) blur(2.4px) opacity(.62);

  /* Feathered well inside its own rectangle, so the plate never shows an edge
     and the bright floor bar at the foot of the composition falls away. */
  --atmos-mask: radial-gradient(ellipse 58% 55% at 50% 50%,
                  #000        0%,
                  #000       60%,
                  transparent 95%);
  -webkit-mask-image: var(--atmos-mask);
          mask-image: var(--atmos-mask);
}

/* The haze between the plate and the lockup. The first gradient is a soft dark
   core sitting directly behind the logo — it is what guarantees the type stays
   readable no matter how bright the plate behind it gets, and it is far more
   predictable than trying to punch a matching hole in the plate's own mask. */
.origin__glow {
  position: absolute; inset: 0;
  z-index: 1;
  background:
    radial-gradient(34% 30% at 50% 45%, rgba(246, 245, 242, .88), transparent 74%),
    radial-gradient(46% 40% at 50% 46%, var(--gold-mist), transparent 72%);
}
/* on the warm wall the dark core would be a smudge, so light mode drops it */
.origin__wrap {
  position: relative;
  z-index: 2;
  display: grid; justify-items: center; gap: clamp(22px, 3.4vh, 38px); text-align: center;
}
.origin__logo { width: min(52vw, 400px); }
.origin__sub {
  margin: 0;
  font-size: clamp(.95rem, 1.5vw, 1.15rem);
  /* the line is a statement now rather than a list of departments, so it is set
     bold — everything else about it is untouched */
  font-weight: 700;
  letter-spacing: .04em;
  color: var(--text-2);
}
.origin__where { color: var(--text-3); }

.cue {
  position: absolute;
  bottom: clamp(28px, 6vh, 60px);
  left: 50%; transform: translateX(-50%);
  display: grid; justify-items: center; gap: 12px;
}
.cue__line {
  position: relative;
  width: 1px; height: 54px;
  background: var(--line-2);
  overflow: hidden;
}
.cue__line::after {
  content: ""; position: absolute; inset: 0;
  background: var(--gold);
  animation: cueFall 2.8s var(--ease-io) infinite;
}
@keyframes cueFall {
  0%   { transform: translateY(-100%); }
  55%  { transform: translateY(100%); }
  100% { transform: translateY(100%); }
}
.cue__text { font-size: .6rem; transition: color .4s var(--ease); }
.cue:hover .cue__text { color: var(--gold); }


/* ═══════════════════════════════════════════ 02 · COMING SOON ═══════════ */
/* A single scene with two products floating in it. The plates are delivered
   with their photographic background already removed, so there is no card, no
   border and no panel - just the devices, a soft pool of light under each, and
   a lot of black. Depth comes from the scene's perspective, which the scroll
   timeline moves the products through. */
.coming .stage { perspective: 1500px; }

.coming__scene {
  width: min(100% - var(--gutter) * 2, 1480px);
  margin-inline: auto;
  height: 100%;
  /* a grid item defaults to min-height:auto, which lets content push it past
     the pinned stage; without this the scene overflows on a short screen */
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  align-content: center;
  gap: clamp(26px, 4.6vh, 60px);
  padding-block: clamp(58px, 9vh, 120px);
  transform-style: preserve-3d;
}

.coming__head { display: grid; justify-items: center; gap: clamp(14px, 2vh, 24px); text-align: center; }

/* the announcement itself - gold, tracked wide, set at display size */
.coming__title {
  margin: 0;
  font-size: clamp(2.1rem, 6.2vw, 5.2rem);
  font-weight: 200;
  text-transform: uppercase;
  letter-spacing: .16em;
  line-height: 1;
  color: var(--gold);
  text-indent: .16em;            /* balances the trailing letter-space */
  will-change: transform, opacity, letter-spacing;
}
.coming__lead {
  margin: 0;
  max-width: 46ch;
  text-wrap: balance;
  font-size: clamp(1rem, 1.4vw, 1.35rem);
  font-weight: 300;
  line-height: 1.6;
  color: var(--text-2);
  will-change: transform, opacity;
}

.coming__products {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(24px, 5vw, 90px);
  align-items: end;
  justify-items: center;
  min-height: 0;
  transform-style: preserve-3d;
}

.product {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: clamp(16px, 2.4vh, 30px);
  justify-items: center;
  width: 100%;
  min-height: 0;
  transform-style: preserve-3d;
  will-change: transform, opacity;
}

.product__shot {
  position: relative;
  display: grid;
  place-items: center;
  width: 100%;
  min-height: 0;
  transform-style: preserve-3d;
}
.product__shot img {
  max-width: 100%;
  /* Capped against the viewport rather than against the grid row: the row is a
     1fr whose height stays indefinite, so a percentage max-height resolves to
     none and the device renders at natural size - which overflowed the pinned
     stage on a short laptop screen and clipped the product names. */
  max-height: min(100%, clamp(132px, 29vh, 330px));
  width: auto; height: auto;
  object-fit: contain;
  /* never blurred - the entrance is depth and opacity only */
  filter: drop-shadow(0 26px 34px rgba(28, 24, 16, .26));
}

/* the pool of light the device floats above, instead of a card */
.product__glow {
  position: absolute;
  left: 50%; bottom: -6%;
  translate: -50% 0;
  width: 78%; height: 26%;
  border-radius: 50%;
  background: radial-gradient(ellipse at center,
              rgba(28, 24, 16, .22) 0%,
              rgba(28, 24, 16, .10) 42%,
              transparent 74%);
  filter: blur(16px);
  pointer-events: none;
}

.product__meta { display: grid; justify-items: center; gap: 8px; text-align: center; will-change: transform, opacity; }
.product__name {
  margin: 0;
  font-size: clamp(1.1rem, 1.9vw, 1.75rem);
  font-weight: 250;
  letter-spacing: -.028em;
  line-height: 1.1;
}
.product__tag {
  margin: 0;
  font-size: .64rem;
  font-weight: 500;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--gold);
}

/* ═══════════════════════════════════════════════════ 03 · CAMERA ════════ */
/* The plate is 800x443 and is delivered at its own size — a hero that fills the
   viewport would mean enlarging it, and an upscale is the one thing that would
   actually soften it. So it is a large centred macro that never exceeds 1:1,
   pushed from 0.86 up to exactly native: the sharpest frame in the whole move
   is the one it comes to rest on.

   Nothing is laid over it. It carried a feathered edge mask before, which read
   as a smudge around the product — the plate's own ground is already dark, so
   it sits on the stage without help. */
/* ---- the invitation and the plate, as one block ----------------------------
   The plate used to float in the middle of a full-height stage with a band of
   empty page above and below it, and the heading lived somewhere else entirely.
   This is a two-row grid that fills the stage instead: the invitation, then the
   photograph, sized together so the section has no dead area. */
.lens {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-rows: auto auto;
  align-content: center;
  justify-items: center;
  gap: clamp(18px, 3.2vh, 46px);
  padding: clamp(70px, 9vh, 120px) var(--gutter) clamp(50px, 7vh, 90px);
  min-height: 0;
  perspective: 1300px;
  pointer-events: none;
}
.lens__head { display: grid; justify-items: center; gap: 12px; text-align: center; }
.lens__title {
  margin: 0;
  font-size: clamp(2rem, 4.9vw, 4rem);
  font-weight: 300;
  letter-spacing: -.035em;
  line-height: 1.02;
  color: var(--text);
  will-change: transform, opacity;
}
.lens__title em { font-style: normal; color: var(--gold); }

.camera__shot {
  margin: 0;
  /* The delivered plate is 650px of pure product — the dark studio ground was
     cropped off in the asset step, so what is left has no rectangle to hide.
     It is allowed up to about 1.35x here so it carries the section, and no
     further: past that the lens rims start to soften, and softening this image
     is the one thing the brief rules out. */
  width: min(94vw, 880px);
  max-height: 100%;
  min-height: 0;
  will-change: transform;
}
.camera__shot img {
  width: 100%;
  height: auto;
  max-height: 100%;
  object-fit: contain;            /* never stretched, never distorted */
  /* The outermost few percent of each edge is taken to transparent so the plate
     dissolves into the page instead of ending on a rule. Two linear masks that
     intersect, not a vignette: the fade is 6% at the sides and 4.5% top and
     bottom, which is the frame edge only. Everything inside that margin — the
     whole product — is at full alpha, unmasked and unblurred. */
  --fade-x: linear-gradient(to right,
              transparent 0%, rgba(0,0,0,.6) 2%, #000 6%,
              #000 94%, rgba(0,0,0,.6) 98%, transparent 100%);
  --fade-y: linear-gradient(to bottom,
              transparent 0%, rgba(0,0,0,.6) 1.5%, #000 4.5%,
              #000 95.5%, rgba(0,0,0,.6) 98.5%, transparent 100%);
  -webkit-mask-image: var(--fade-x), var(--fade-y);
          mask-image: var(--fade-x), var(--fade-y);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}

/* Light, not instrumentation — the rings, graduations and focus readout are
   gone with the section's old purpose. This is the glow gathering in the glass
   as the frame comes in. */
.optic {
  position: absolute; inset: 0;
  pointer-events: none;
  perspective: 1200px;
}
.optic > i {
  position: absolute;
  left: 50%; top: 52%;
  translate: -50% -50%;
  border-radius: 50%;
  opacity: 0;
  will-change: transform, opacity;
}
.optic__bloom {
  width: 96vmin; height: 96vmin;
  background: radial-gradient(circle,
              transparent 0%,
              transparent 34%,
              var(--gold-mist) 54%,
              transparent 78%);
  filter: blur(24px);
}

/* ══════════════════════════════════ 03b · THE PORTAL (inside camera) ════ */
/* What the aperture opens onto, in the same stage as the macro so the passage
   through the lens is one continuous move. The rings are not drawn at different
   sizes to fake depth - they are one ring at different distances in a real
   perspective, and the timeline flies them past the viewer. */
.portal {
  position: absolute; inset: 0;
  background: var(--void);
  opacity: 0;
  pointer-events: none;
}
.portal__tunnel {
  position: absolute; inset: 0;
  perspective: 760px;
  perspective-origin: 50% 50%;
  transform-style: preserve-3d;
  will-change: transform;
}
.portal__tunnel i {
  position: absolute;
  left: 50%; top: 50%;
  margin: -29vmin 0 0 -29vmin;
  width: 58vmin; height: 58vmin;
  border-radius: 50%;
  border: 1px solid var(--gold-soft);
  box-shadow: 0 0 30px -10px var(--gold-soft);
  will-change: transform, opacity;
}
/* every third ring is a plain hairline, so the tunnel has texture instead of
   being a stack of identical circles */
.portal__tunnel i:nth-child(3n) {
  border-color: rgba(22, 22, 28, .14);
  box-shadow: none;
}

/* the far point everything is travelling toward */
.portal__core {
  position: absolute;
  left: 50%; top: 50%;
  translate: -50% -50%;
  width: 44vmin; height: 44vmin;
  border-radius: 50%;
  background: radial-gradient(circle,
              rgba(184, 144, 44, .20) 0%,
              var(--gold-mist) 24%,
              transparent 66%);
  filter: blur(10px);
  opacity: 0;
}


/* ══════════════════════════════════ 03c · EXPLORE (inside camera) ═══════ */
/* The categories arrive out of the same depth the rings came from, at the far
   end of the lens journey they belong to. This layer shares the camera's stage
   and its perspective, so nothing is cut to. */
.explore {
  position: absolute;
  inset: 0;
  display: grid;
  /* both rows sized to content and centred as a group - a 1fr here would
     absorb all the slack and leave the grid stranded below the heading */
  grid-template-rows: auto auto;
  align-content: center;
  justify-content: center;
  gap: clamp(20px, 3.4vh, 44px);
  width: min(100% - var(--gutter) * 2, 1320px);
  margin-inline: auto;
  padding-block: clamp(64px, 9vh, 110px);
  min-height: 0;
  perspective: 1300px;
  opacity: 0;
  pointer-events: none;
}
.explore.is-live { pointer-events: auto; }

/* The grid introduces itself properly: a section heading, then the line under
   it, then the cards. */
.explore__head { display: grid; justify-items: center; gap: clamp(6px, .9vh, 12px); text-align: center; }
.explore__title {
  margin: 0;
  font-size: clamp(1.6rem, 3.5vw, 3rem);
  font-weight: 300;
  letter-spacing: -.032em;
  line-height: 1.04;
  color: var(--text);
  will-change: transform, opacity;
}
/* the second half carries the accent, not the whole line */
.explore__title em { font-style: normal; color: var(--gold); }
.explore__lead {
  margin: 0;
  font-size: clamp(.86rem, 1.15vw, 1.05rem);
  font-weight: 300;
  color: var(--text-2);
  will-change: transform, opacity;
}

/* Three across. Not fixed at nine - a tenth card starts a fourth row and the
   reveal picks it up, because the timeline is built from whatever is rendered. */
.cats {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(10px, 1.4vw, 20px);
  width: 100%;
  min-height: 0;
  transform-style: preserve-3d;
}

/* A card built around its picture. The image area is a fixed ratio so any
   product shot drops in without changing the grid, and the type block below it
   always starts on the same line across the row. Minimal by design: one
   hairline, one soft shadow, and a lift on approach. */
.cat {
  position: relative;
  display: grid;
  grid-template-rows: auto auto;
  gap: clamp(12px, 1.2vw, 18px);
  padding: clamp(12px, 1.1vw, 18px);
  min-height: 0;
  border-radius: clamp(14px, 1.2vw, 20px);
  background: var(--raise);
  box-shadow:
    inset 0 0 0 1px var(--cine-line),
    0 14px 30px -24px rgba(28, 24, 16, .40);
  transform-style: preserve-3d;
  will-change: transform, opacity;
  transition: transform .55s var(--ease), box-shadow .55s var(--ease);
}

/* the lift: a small rise and the faintest tilt, not a card flip */
@media (hover: hover) and (pointer: fine) {
  .cat:hover {
    transform: translateY(-6px) rotateX(2.2deg) rotateY(-2.2deg) scale(1.012);
    box-shadow:
      inset 0 0 0 1px var(--gold-soft),
      0 26px 48px -26px rgba(28, 24, 16, .38);
  }
  .cat:hover .cat__go { width: 26px; }
}

/* The image-ready frame. A fixed ratio and contain, so a tall bottle of a
   product and a wide one both sit correctly without cropping. */
/* One frame, two kinds of picture in it. The shape is the same on every card
   so the grid stays a grid; what changes is whether the picture floats in the
   frame or fills it. Neither is ever stretched — both are `contain`, and the
   photographs were already cut to this ratio in the asset step, so they meet
   the corners without a letterbox and without CSS choosing what to lose. */
.cat__media {
  position: relative;
  aspect-ratio: 4 / 3;
  border-radius: clamp(9px, .8vw, 13px);
  display: grid;
  /* One explicit cell, not an auto one. With auto rows a tall picture's own
     min-content height wins over the aspect-ratio and the frame grows to fit
     it — the portrait screen-protector shot burst its card exactly that way.
     1fr takes its size from the frame, and min-*: 0 below lifts the automatic
     minimum that would otherwise put it back. */
  grid-template: 1fr / 1fr;
  place-items: center;
  padding: clamp(8px, .8vw, 14px);
  /* no fill: a product sits on the card itself, so there is no grey square
     boxed around it. The frame still reserves its space. */
  background: transparent;
}
.cat__media img {
  width: 100%; height: 100%;
  min-width: 0; min-height: 0;
  object-fit: contain;            /* never crops a product, never stretches it */
  object-position: center;
}

/* A product with its ground removed. It floats, so it is given room: more
   padding than the frame's default, and nothing drawn behind it. */
.cat__media--cut { padding: clamp(10px, 1.1vw, 18px); }

/* A photograph of the shop or the bench. There is no product to lift out of
   these — the frame is the subject — so it fills the plate edge to edge,
   rounded, with a hairline to seat it on the card rather than bleed off it. */
.cat__media--photo {
  padding: 0;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(22, 22, 28, .07);
}
.cat__media--photo img { border-radius: inherit; }
.cat__media--empty::before {
  content: "";
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 1px solid var(--line-2);
}

.cat__body {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 4px 12px;
  padding: 0 4px 4px;
  min-width: 0;
}
.cat__name {
  margin: 0;
  grid-column: 1;
  font-size: clamp(.95rem, 1.05vw, 1.1rem);
  font-weight: 450;
  letter-spacing: -.018em;
  line-height: 1.15;
}
.cat__note {
  margin: 0;
  grid-column: 1 / -1;
  font-size: clamp(.7rem, .78vw, .8rem);
  line-height: 1.45;
  color: var(--text-3);
}
/* a small gold tell that grows on approach */
.cat__go {
  grid-column: 2; grid-row: 1;
  width: 14px; height: 1px;
  background: var(--gold);
  transition: width .55s var(--ease);
}

a.cat { text-decoration: none; color: inherit; }


/* ═══════════════════════════════════════════════════ 05 · ISLAND ════════ */
/* Fills the stage. `cover` is also what the overlay maths assumes — the pill is
   placed from the image's real rect inside this box, computed the cover way —
   so this and js/app.js have to agree. */
.island__shot { position: absolute; inset: 0; margin: 0; }
.island__shot img {
  width: 100%; height: 100%;
  object-fit: cover;
  object-position: center top;
  will-change: transform;
}

/* The overlay pill is positioned from the measured location of the real
   Dynamic Island in the photograph, so it lands exactly on it. */
.pill {
  /* a pre-script fallback only; app.js resolves these from the plate's real
     rendered geometry as soon as the photograph has loaded */
  --pill-cx: 48.71%;
  --pill-cy: 13.47%;
  position: absolute;
  left: var(--pill-cx); top: var(--pill-cy);
  translate: -50% -50%;
  width: 19.08%;
  aspect-ratio: 229 / 66;
  border-radius: 999px;
  background: #000;
  display: grid;
  place-items: center;
  overflow: hidden;
  will-change: width, aspect-ratio, transform;
}
/* The island opens into the page rather than displaying anything. As the zoom
   arrives it widens and its inside goes from black to a lit aperture, and the
   next layer comes up out of it. */
.pill__mouth {
  position: absolute; inset: 0;
  border-radius: inherit;
  /* black first. The only light in here is a thin sheen across the top, the way
     glass catches a room — no gold fill. */
  background:
    linear-gradient(178deg, rgba(255,255,255,.085) 0%, transparent 26%),
    radial-gradient(120% 150% at 50% 128%, rgba(255,255,255,.05), transparent 60%);
  opacity: 0;
}

/* ═════════════════════════════ inside the island (Spotlight) ════════════ */
/* What the deep zoom opens onto. It shares the stage and its perspective with
   the device that was just entered, so nothing is cut to — and it sits on the
   exact cues the timeline already used, which is why the zoom above is
   untouched. */
.inside {
  position: absolute;
  inset: 0;
  display: grid;
  /* the carousel takes what is left after the heading and the link, so the
     cards can shrink to the screen instead of running off the bottom of it */
  grid-template-rows: auto minmax(0, 1fr) auto;
  justify-items: center;
  align-items: center;
  gap: clamp(14px, 2.4vh, 32px);
  width: min(100% - var(--gutter) * 2, 1400px);
  margin-inline: auto;
  padding-block: clamp(44px, 6vh, 84px);
  min-height: 0;
  perspective: 1300px;
  opacity: 0;
  pointer-events: none;
}
/* pointer-events is turned on by the island timeline at the moment the zoom
   opens onto this section, and taken away again if you scrub back — see
   js/app.js. Nothing here is clickable before it has arrived. */

.inside__head { display: grid; justify-items: center; gap: 10px; text-align: center; }
.inside__title {
  margin: 0;
  font-size: clamp(1.7rem, 4vw, 3.1rem);
  font-weight: 200;
  letter-spacing: -.03em;
  line-height: 1.04;
  color: var(--text);
  will-change: transform, opacity;
}
.inside__title em { font-style: normal; color: var(--gold); }
.inside__lead {
  margin: 0;
  font-size: clamp(.86rem, 1.1vw, 1.02rem);
  font-weight: 300;
  color: var(--text-2);
  will-change: transform, opacity;
}


/* What the island is holding while the camera closes on it. It scales with the
   plate, so it grows as the viewer approaches and is passed through rather than
   dismissed. Sized in vmin so it stays proportionate on any screen. */
.pill__now {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 1.1vmin;
  opacity: 0;
  pointer-events: none;
}
/* Gold is the accent, and this is the only thing inside the island wearing it —
   the island itself stays black, which is what keeps it reading as a cut-out. */
.pill__title {
  margin: 0;
  font-size: 4.2vmin;
  font-weight: 700;
  letter-spacing: -.03em;
  line-height: 1;
  color: var(--gold-hi);
  text-shadow: 0 .2vmin 2.2vmin rgba(0, 0, 0, .6);
}

/* The music tell: seven white bars at their own heights, breathing at their own
   rates. No panel, no artwork, no transport icons — at this size anything more
   is clutter, and a phone showing music does not show them either. */
.pill__eq {
  display: flex;
  align-items: flex-end;
  gap: .46vmin;
  height: 2.3vmin;
}
.pill__eq i {
  width: .40vmin;
  height: 100%;
  border-radius: .22vmin;
  background: rgba(255, 255, 255, .92);
  transform-origin: bottom;
  animation: eq 1.15s var(--ease-io) infinite alternate;
}
.pill__eq i:nth-child(1) { animation-delay: 0s;    animation-duration: 1.05s }
.pill__eq i:nth-child(2) { animation-delay: .17s;  animation-duration: .82s  }
.pill__eq i:nth-child(3) { animation-delay: .31s;  animation-duration: 1.28s }
.pill__eq i:nth-child(4) { animation-delay: .09s;  animation-duration: .94s  }
.pill__eq i:nth-child(5) { animation-delay: .42s;  animation-duration: 1.12s }
.pill__eq i:nth-child(6) { animation-delay: .24s;  animation-duration: .88s  }
.pill__eq i:nth-child(7) { animation-delay: .55s;  animation-duration: 1.2s  }
@keyframes eq { from { transform: scaleY(.20) } to { transform: scaleY(1) } }
/* Paused until the scene is within a screen of the viewport — js/app.js adds
   is-near. The bars are inside a black cut-out the zoom has not reached yet, so
   there is nothing to see while they are stopped, and seven infinite animations
   stop ticking for the length of the page. */
.pill__eq i { animation-play-state: paused; }
#island.is-near .pill__eq i { animation-play-state: running; }


/* ---- the display's own furniture ------------------------------------------
   Placed in px by js/app.js from the plate's measured geometry, for the same
   reason the pill is: the photograph is drawn with object-fit: cover, so a
   percentage of the figure is not a percentage of the picture. */
/* ═══════════════════════════════════════════ 05 · WHAT WE DO ════════════ */
/* Six services on one grid of hairlines. The rules are the layout — no boxes,
   no borders drawn around anything — so the section reads as part of the page
   rather than a component dropped onto it. */
.doing__head {
  display: grid; gap: 20px;
  max-width: 46ch;
  margin-bottom: clamp(46px, 7vh, 84px);
}
/* The section names itself. It used to be a tracked-out eyebrow reading "Noble
   Mobile", which said nothing the page had not already said twice. */
.doing__kicker {
  margin: 0;
  font-size: clamp(1.05rem, 1.6vw, 1.45rem);
  font-weight: 500;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--gold);
}

.deeds {
  margin: 0; padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1px;
  background: var(--cine-line);
  border-block: 1px solid var(--cine-line);
}
.deed {
  position: relative;
  display: grid;
  align-content: start;
  gap: 12px;
  padding: clamp(26px, 3vw, 46px) clamp(20px, 2.2vw, 38px);
  background: var(--cine);
  transform-style: preserve-3d;
  will-change: transform, opacity;
}
.deed::before {
  content: "";
  position: absolute; inset: 0;
  background: radial-gradient(120% 90% at 50% 118%, var(--gold-mist), transparent 72%);
  opacity: 0;
  transition: opacity .6s var(--ease);
  pointer-events: none;
}
.deed:hover::before { opacity: 1; }
/* Up from .68rem: at that size the counters were a texture rather than a
   label. Still the smallest thing in the card — they number the services, they
   do not compete with their names. */
.deed__n {
  font-size: clamp(.86rem, 1.05vw, 1rem);
  font-weight: 500;
  letter-spacing: .16em;
  color: var(--gold);
}
.deed__name {
  margin: 0;
  font-size: clamp(1.15rem, 1.75vw, 1.6rem);
  font-weight: 250;
  letter-spacing: -.026em;
  line-height: 1.12;
}
.deed__note {
  margin: 0;
  max-width: 32ch;
  font-size: clamp(.9rem, 1vw, .99rem);
  line-height: 1.6;
  color: var(--text-2);
}

.doing__foot {
  margin: clamp(34px, 5vh, 58px) 0 0;
  font-size: .9rem;
  letter-spacing: .02em;
  color: var(--text-2);
}

/* ═══════════════════════════════════════════════ Spotlight cards ════════ */
/* ─────────────────────────────── the customer carousel ────────────────────
   Eleven real photographs of real people, six to a screen on desktop and the
   rest one page to the right. The track is transformed rather than scrolled so
   the paging can be eased properly — a native smooth-scroll arrives too fast
   for the rest of this page — and a pointer drag is wired to the same
   transform, which is what makes it swipe on a phone.

   --spot-n is the only thing that changes between breakpoints. Everything else
   — card width, page width, how many dots there are — is derived from it, in
   the CSS and in js/app.js alike. */
.spot {
  --spot-n: 6;
  --spot-gap: clamp(10px, 1.1vw, 18px);
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  /* Content-height, capped at the row. On a desktop that means the cards are
     exactly as tall as they need to be — stretched to the row they gained a
     band of empty card under the words. On a short screen the cap binds, the
     row shrinks, and the frame gives way first. */
  align-self: center;
  max-height: 100%;
  gap: clamp(14px, 2vh, 26px);
  /* Wider than the column it sits in, deliberately. Six across is the brief,
     and six across inside the section's own measure makes each photograph
     about 200px — enough to see a face, but not by much. Reclaiming the
     section's gutters buys every card another 10-25% depending on the screen,
     which is the difference between a thumbnail and a portrait.

     Measured from the section rather than from 100vw: vw includes the
     scrollbar, and on a desktop with a classic scrollbar that is exactly
     enough to push the last card under the stage's clip. */
  width: min(calc(100% + 2 * var(--gutter) - 28px), 1640px);
  justify-self: center;
  min-height: 0;
}

.spot__viewport {
  overflow: hidden;
  min-height: 0;
  /* room inside the clip for a card to lift and cast without being cut off */
  padding: 16px 0 26px;
  margin: -16px 0 -26px;
  perspective: 1200px;
}
.spot__track {
  display: flex;
  gap: var(--spot-gap);
  align-items: stretch;      /* every card the same height, whatever it says */
  height: 100%;
  min-height: 0;
  transform-style: preserve-3d;
  will-change: transform;
  touch-action: pan-y;            /* vertical scroll stays the page's */
}
.spot__track.is-dragging { cursor: grabbing; }

/* ---- the card ------------------------------------------------------------ */
.tcard {
  flex: 0 0 calc((100% - (var(--spot-n) - 1) * var(--spot-gap)) / var(--spot-n));
  margin: 0;
  /* A column rather than fixed rows: the frame is the only part allowed to
     give, so on a short screen the picture area shrinks and the words stay
     whole. Nothing is cropped when it does — the photograph is contained, so
     it simply sits in a slightly wider letterbox. */
  display: flex;
  flex-direction: column;
  min-height: 0;
  gap: clamp(8px, .8vw, 13px);
  padding: clamp(9px, .85vw, 14px);
  border-radius: clamp(14px, 1.25vw, 20px);
  background: linear-gradient(168deg, #fff, rgba(255, 255, 255, .72));
  box-shadow:
    inset 0 0 0 1px var(--cine-line),
    0 14px 30px -24px rgba(28, 24, 16, .40);
  transform-style: preserve-3d;
  will-change: transform, opacity;
  transition: box-shadow .5s var(--ease);
}
/* the depth on approach is a shadow change only — the movement is GSAP's, so
   the two never write to the same property */
.tcard.is-hot {
  box-shadow:
    inset 0 0 0 1px var(--gold-soft),
    0 34px 56px -30px rgba(28, 24, 16, .42);
}

/* The photograph sits in a frame of one fixed shape, and is contained inside
   it rather than filling it. The supplied frames run .749 to .810; at 7/9 the
   widest letterbox any of them takes is about 3.5%, which is why the shape can
   be consistent without a single face being cropped. */
.tcard__frame {
  position: relative;
  flex: 0 1 auto;
  min-height: 0;
  aspect-ratio: 7 / 9;
  border-radius: clamp(9px, .85vw, 14px);
  overflow: hidden;
  background:
    linear-gradient(170deg, rgba(22, 22, 28, .030), rgba(22, 22, 28, .055));
  transform: translateZ(0);
}
.tcard__img {
  width: 100%;
  height: 100%;
  object-fit: contain;            /* never cropped, never stretched */
  object-position: center;
  display: block;
  /* no will-change here: the only thing that moves this is the hover scale, and
     js/app.js promotes it for the length of that and hands the layer back
     afterwards. Eleven of these held one each for the life of the page. */
}

.tcard__stars {
  flex: 0 0 auto;
  display: flex;
  gap: .14em;
  font-size: clamp(.58rem, .66vw, .7rem);
  line-height: 1;
  color: var(--gold);
  letter-spacing: .04em;
}

.tcard__text {
  margin: 0;
  flex: 0 0 auto;
  font-size: clamp(.7rem, .78vw, .82rem);
  font-weight: 350;
  line-height: 1.5;
  letter-spacing: -.006em;
  color: var(--text-2);
  text-wrap: pretty;
}
.tcard__name {
  margin: 0;
  font-size: clamp(.64rem, .7vw, .74rem);
  letter-spacing: .04em;
  color: var(--text-3);
}

/* Reserved frames, shown only if the photographs are ever taken back out. The
   wall never invents a face to fill a gap. */
.tcard--empty .tcard__frame {
  background:
    repeating-linear-gradient(135deg, rgba(22,22,28,.030) 0 12px, transparent 12px 24px),
    rgba(22, 22, 28, .026);
}
.tcard--empty .tcard__text {
  font-size: .58rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ---- arrows and the page indicator --------------------------------------- */
.spot__controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(12px, 1.4vw, 22px);
}
.spot__arrow {
  display: grid;
  place-items: center;
  width: clamp(34px, 3vw, 42px);
  aspect-ratio: 1;
  border-radius: 50%;
  color: var(--text-2);
  box-shadow: inset 0 0 0 1px var(--line-2);
  background: var(--raise);
  transition: color .35s var(--ease), box-shadow .35s var(--ease),
              transform .35s var(--ease), opacity .35s var(--ease);
}
.spot__arrow svg { width: 46%; height: 46%; }
.spot__arrow:hover:not(:disabled) {
  color: var(--gold);
  box-shadow: inset 0 0 0 1px var(--gold-soft);
  transform: scale(1.06);
}
.spot__arrow:disabled { opacity: .28; cursor: default; }

.spot__dots { display: flex; align-items: center; gap: 8px; }
.spot__dot {
  width: 7px; height: 7px;
  padding: 0;
  border-radius: 50%;
  background: var(--line-2);
  transition: background .4s var(--ease), transform .4s var(--ease);
}
.spot__dot[aria-selected="true"] {
  background: var(--gold);
  transform: scale(1.34);
}

.ig {
  display: inline-flex; align-items: center; gap: 16px;
  padding: 13px 22px 13px 15px;
  border-radius: 999px;
  box-shadow: inset 0 0 0 1px var(--cine-line);
  will-change: transform, opacity;
  transition: box-shadow .5s var(--ease), background-color .5s var(--ease);
}
.ig:hover { box-shadow: inset 0 0 0 1px var(--gold-soft); background: var(--gold-mist); }
/* Instagram's mark, drawn as SVG in Instagram's own gradient. It is the one
   thing on this page that is not gold: a brand icon tinted to match a site is
   no longer the brand icon, and this is the button people look for. */
.ig__glyph {
  width: 32px; height: 32px;
  flex: 0 0 auto;
  border-radius: 9.4px;
  overflow: hidden;
  display: block;
  box-shadow: 0 4px 12px -6px rgba(120, 40, 120, .55);
}
.ig__glyph svg { width: 100%; height: 100%; display: block; }
.ig__text { display: grid; gap: 2px; text-align: left; }
.ig__text b { font-weight: 350; font-size: .92rem; }
.ig__text small { font-size: .72rem; color: var(--text-2); }
.ig__arrow { width: 18px; height: 1px; background: var(--gold); transition: width .4s var(--ease); }
.ig:hover .ig__arrow { width: 28px; }

/* ═══════════════════════════════════════════════════ footer ═════════════ */
.foot {
  position: relative;
  background: transparent;          /* the sky carries through */
  padding: clamp(76px, 12vh, 140px) 0 34px;
}
/* the last of the descent settling out, instead of a ruled-off block */
.foot::before {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 0;
  height: clamp(90px, 14vh, 170px);
  background: linear-gradient(var(--ink), transparent);
  pointer-events: none;
}
.foot::after {
  content: "";
  position: absolute;
  left: 50%; top: 0;
  translate: -50% 0;
  width: min(70%, 900px); height: 1px;
  background: linear-gradient(90deg, transparent, var(--line-2), transparent);
}
/* Three columns, not five. The Shop and Service lists came out — they pointed
   at four sections between them and repeated the nav — and the space they left
   went to the things people actually come down here for: who we are, where we
   are, and how to reach someone. */
.foot__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr) minmax(0, .95fr);
  gap: clamp(34px, 5vw, 78px);
  padding-bottom: clamp(44px, 6.5vh, 76px);
  border-bottom: 1px solid var(--line);
}

.foot__brand { display: grid; gap: 16px; align-content: start; }
.foot__mark { width: 46px; }
.foot__name {
  margin: 0;
  font-size: 1.02rem;
  font-weight: 400;
  letter-spacing: -.018em;
}
.foot__blurb {
  margin: 0;
  max-width: 34ch;
  font-size: .88rem;
  color: var(--text-2);
  line-height: 1.72;
}

.foot__col { display: grid; gap: 14px; align-content: start; }
.foot__col .label { margin-bottom: 2px; }
/* :not(.btn) matters here — a plain .btn--gold loses to .foot__col a on
   specificity, which painted the white label on the gold button back to body
   text and made it unreadable. */
.foot__col a:not(.btn) { color: var(--text-2); transition: color .35s var(--ease); }
.foot__col a:not(.btn):hover { color: var(--gold); }

/* A description list, because that is what this is: a label and its value.
   The grid keeps the labels in their own column so the values line up. */
.fact {
  margin: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 10px clamp(14px, 1.6vw, 22px);
  font-size: .88rem;
  line-height: 1.6;
}
.fact dt {
  font-size: .64rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text-3);
  padding-top: .28em;
  white-space: nowrap;
}
.fact dd { margin: 0; color: var(--text-2); }
.fact dd a { display: block; }

.foot__col--act { align-content: start; }
.foot__pitch {
  margin: 0;
  max-width: 30ch;
  font-size: .86rem;
  color: var(--text-2);
  line-height: 1.7;
}

/* A text link with a rule that draws itself on approach, rather than a second
   filled button competing with the one on the Visit page. The arrow moves, the
   words do not. */
.foot__go {
  justify-self: start;
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding-bottom: 4px;
  font-size: .88rem;
  color: var(--text);
  border-bottom: 1px solid var(--line-2);
  transition: color .4s var(--ease), border-color .4s var(--ease);
}
.foot__go span {
  display: inline-block;
  transition: transform .4s var(--ease);
}
.foot__go:hover { color: var(--gold); border-bottom-color: var(--gold-soft); }
.foot__go:hover span { transform: translateX(5px); }

/* The Visit page closes rather than sells: the last column is a thank-you, set
   larger and lighter than anything around it. */
.foot__col--close { align-content: start; gap: 12px; }
.foot__thanks {
  margin: 0;
  max-width: 18ch;
  font-size: clamp(1.15rem, 1.7vw, 1.5rem);
  font-weight: 300;
  letter-spacing: -.028em;
  line-height: 1.2;
  color: var(--text);
  text-wrap: balance;
}

/* ---- the one button shape the site uses ---------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 12px 24px;
  border-radius: 999px;
  font-size: .85rem;
  letter-spacing: -.005em;
  box-shadow: inset 0 0 0 1px var(--line-2);
  transition: background-color .4s var(--ease), color .4s var(--ease),
              box-shadow .4s var(--ease), transform .4s var(--ease);
}
.btn:hover { transform: translateY(-2px); box-shadow: inset 0 0 0 1px var(--gold-soft); }
.btn--gold {
  justify-self: start;
  background: var(--gold);
  color: #fff;
  box-shadow: inset 0 0 0 1px var(--gold);
}
.btn--gold:hover {
  background: var(--gold-hi);
  box-shadow: inset 0 0 0 1px var(--gold-hi), 0 14px 26px -14px rgba(138, 103, 18, .6);
}

/* ---- the social marks ---------------------------------------------------- */
/* Each in its own brand's colours. They are the one place on this site that is
   not gold, because a brand mark tinted to match a page stops being the mark
   people are scanning for. */
.soc { display: flex; gap: 12px; margin-top: 4px; }
.soc__btn {
  display: block;
  width: 38px; height: 38px;
  border-radius: 11px;
  overflow: hidden;
  box-shadow: 0 6px 16px -10px rgba(28, 24, 16, .55);
  transition: transform .45s var(--ease), box-shadow .45s var(--ease);
}
.soc__btn svg { display: block; width: 100%; height: 100%; }
.soc__btn:hover {
  transform: translateY(-3px) scale(1.05);
  box-shadow: 0 14px 24px -12px rgba(28, 24, 16, .6);
}

.foot__base {
  display: flex; flex-wrap: wrap; gap: 10px 30px;
  justify-content: space-between; align-items: center;
  padding-top: 26px;
}
.foot__base p { margin: 0; font-size: .74rem; color: var(--text-3); }
.foot__fine { max-width: 52ch; }


/* ═══════════════════════════════════════════════════ cursor ═════════════ */
.cursor {
  position: fixed; top: 0; left: 0; z-index: 300;
  width: 7px; height: 7px;
  margin: -3.5px 0 0 -3.5px;
  border-radius: 50%;
  background: var(--gold);
  pointer-events: none;
  transition: width .35s var(--ease), height .35s var(--ease),
              margin .35s var(--ease), opacity .35s var(--ease);
}
.cursor.is-wide { width: 42px; height: 42px; margin: -21px 0 0 -21px; background: transparent;
                  box-shadow: inset 0 0 0 1px var(--gold); }
@media (hover: none), (pointer: coarse) { .cursor { display: none; } }


/* ═══════════════════════════════════════════════════ responsive ═════════ */
@media (max-width: 1100px) {
  .spot { --spot-n: 3; }
  .nav__pill { display: none; }
  .deeds { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .foot__grid { grid-template-columns: 1fr 1fr; gap: clamp(30px, 5vw, 52px); }
}

@media (max-width: 820px) {
  :root { --gutter: clamp(18px, 5vw, 30px); }

  .nav { padding: 12px var(--gutter); }
  .nav__word { width: 88px; }
  .nav__cta { display: none; }

  /* Portrait: the products stack, so they enter from top-left and bottom-right
     instead of the sides. Sized off height so both stay fully visible. */
  .coming__scene { gap: clamp(18px, 3vh, 34px); padding-block: clamp(64px, 9vh, 100px); }
  .coming__title { letter-spacing: .12em; text-indent: .12em; }
  .coming__products { grid-template-columns: 1fr; gap: clamp(16px, 3vh, 30px); align-items: center; }
  .product { grid-template-rows: minmax(0, 1fr) auto; gap: 10px; }
  .product__glow { width: 62%; height: 20%; }

  /* the fixed cinematic scenes get a stacked, readable arrangement */
  /* Two cards to a page on a phone rather than six squeezed onto one: the
     photographs are the content here, and at a sixth of 375px nobody can see
     a face. The carousel re-pages itself off this number. */
  .spot { --spot-n: 2; }
  .inside { gap: clamp(14px, 2.4vh, 28px); padding-block: clamp(48px, 7vh, 80px); }
  .ig { padding: 11px 18px 11px 13px; gap: 12px; }

  /* The grid stays three across on a phone rather than becoming a long list:
     nine cards in two columns would be five rows, which cannot fit a pinned
     screen. Instead the card itself compacts - the supporting line steps
     aside and the frame shrinks. */
  /* A phone's stage is far taller than this plate is, and at 94vw the picture
     could only ever be 240px of an 812px screen — the rest was empty page,
     which is exactly what it must not be. So on a narrow screen the figure
     takes the whole row under the heading and the photograph covers it,
     framed on the lens cluster: it is a closer crop of the same image, never
     stretched and never enlarged past what the file carries. */
  .lens { grid-template-rows: auto minmax(0, 1fr); gap: clamp(14px, 2.4vh, 26px);
          padding: clamp(54px, 7.5vh, 92px) var(--gutter) clamp(40px, 5.5vh, 70px); }
  .camera__shot { width: 100%; height: 100%; }
  .camera__shot img { height: 100%; object-fit: cover; object-position: 64% 50%; }
  .lens__title { font-size: clamp(1.9rem, 8.4vw, 2.6rem); }

  .explore { padding-block: clamp(50px, 7.5vh, 84px); gap: clamp(14px, 2.4vh, 26px); }
  .cats { gap: 8px; }
  .cat { padding: 9px; gap: 7px; border-radius: 10px; }
  .cat__media { min-height: clamp(34px, 6vh, 62px); }
  .cat__media--empty::before { width: 11px; height: 11px; }
  .cat__name { font-size: .76rem; letter-spacing: -.01em; }
  .cat__note { display: none; }



  /* the headline and the list stack, and the list tightens so the whole scene
     still fits one pinned screen */

  /* A phone screen is far narrower than this composition is wide, so the plate
     is scaled off the viewport by a much bigger multiple — at the desktop ratio
     the Apple would shrink to a smudge behind the lockup. Sizing by height
     instead does not work: the global img max-width:100% collapses it. */
  .origin__atmos { width: min(460vw, 1700px); }


  .foot__grid { grid-template-columns: 1fr; }
}

@media (max-width: 520px) {
  .deeds { grid-template-columns: 1fr; }
  .spot { --spot-n: 1; }
  .tcard { padding: 12px; }
  .tcard__text { font-size: .82rem; }
}


/* ═══════════════════════════════════════════════ phones: the full pass ═══
   Everything below is scoped to phone-sized viewports and nothing above 820px
   wide (or, for landscape, 500px tall) can match it, so the desktop layout is
   untouched by construction.

   Three shapes are handled, because a pinned 100vh scene behaves completely
   differently in each:
     - portrait            375x812 and friends
     - short portrait      320x568, 360x640 — the scenes have far less height
     - landscape phone     812x375, 667x375 — very little height at all
   ========================================================================= */
@media (max-width: 820px) {
  /* The header keeps its one action. "Visit us" was hidden below 820px, which
     left a phone with no way to reach the Visit page from the top of the site. */
  .nav__cta {
    display: inline-flex;
    align-items: center;
    min-height: 38px;
    padding: 0 16px;
    font-size: .78rem;
  }
  .nav { padding-block: 8px; }
  .nav__brand { min-height: 44px; }       /* a thumb-sized target around the mark */

  /* Every pinned scene's content starts below the fixed header (60px on a
     phone), not underneath it. */
  .coming__scene { padding-top: max(70px, 9vh); }
  .lens          { padding-top: max(70px, 7.5vh); }
  .explore       { padding-top: max(70px, 7.5vh); }

  /* the smallest type on the page, brought up to a comfortable minimum */
  .cue__text   { font-size: .68rem; }
  .product__tag { font-size: .68rem; }
  .fact dt     { font-size: .68rem; }

  /* taps: phone numbers and links in the footer get a full-height row each */
  .fact dd a { padding-block: 7px; }
  /* the extra height is added outside the text, so each number still starts
     level with its label and the rows keep their rhythm */
  .fact dd a:first-child { margin-top: -7px; }
  .fact dd a:last-child  { margin-bottom: -7px; }
  /* the link's rule stays under its words: the tap area grows invisibly
     around it instead of pushing the rule 30px below the text */
  .foot__go { position: relative; }
  .foot__go::after { content: ""; position: absolute; inset: -12px -8px -10px; }
  .spot__arrow { width: 40px; }

  /* The Spotlight card's photograph. On a phone the frame used to be the part
     that gave way when height ran short, and because it kept the full card
     width the photograph shrank into a narrow strip between two wide bars.
     The frame is now sized off the screen's height and keeps the photograph's
     own shape, so it gets smaller as a whole instead of thinner. */
  .tcard__frame {
    flex: none;
    align-self: center;
    height: clamp(150px, 40vh, 420px);
    width: min(100%, calc(clamp(150px, 40vh, 420px) * 7 / 9));
    aspect-ratio: auto;
  }

  /* the section heading must clear the fixed header */
  .inside { padding-top: max(72px, 9vh); }
}

/* Portrait phones: the two Coming Soon devices stack, and each gets an equal,
   definite half of the space under the heading. Left to size themselves, the
   rows took the photographs' natural heights before their max-height could
   apply — the Pro Max is a tall image — and the second device's label was
   pushed below the bottom of the screen. */
@media (max-width: 820px) and (orientation: portrait) {
  .coming__products {
    grid-template-rows: repeat(2, minmax(0, 1fr));
    align-items: stretch;
    height: 100%;
  }
  .product { height: 100%; }
  .product__shot { height: 100%; }
  /* a % max-height never resolves in that row, so the picture is laid over
     the box instead and contained in it — whole, undistorted, never cropped */
  .product__shot img { position: absolute; inset: 0; width: 100%; height: 100%; max-height: none; }

  /* The Dynamic Island plate, and the island with it, as one piece.
     Filling a tall screen blew the device top up to four times the phone's
     width and showed a slice of it. Here the figure takes the plate's own
     shape (1800x923), so the picture is never cropped or stretched inside it,
     and it is sized so the phone body — the middle 62% of the plate — is 85%
     of the screen. What spills past the edges is only the plate's empty
     ground either side of the handset. The island overlay is placed by
     js/app.js from this box, so it scales and moves with the phone. */
  /* Centred with offsets, not `translate`: the timeline scales and moves this
     figure, and GSAP owns its transform — a CSS translate here gets folded
     into its numbers once and then goes stale when the screen changes. */
  .island__shot {
    inset: auto;
    width: 137vw;
    height: calc(137vw * 923 / 1800);
    left: calc(50% - 68.5vw);
    top: calc(46% - 137vw * 923 / 3600);
  }
  /* what the island holds is sized to the island, not to the screen, so it
     keeps the desktop proportions on the smaller cut-out */
  /* and no layer hint: a promoted layer is painted once at the island's small
     size and then magnified, which softens the type as the zoom runs deep —
     unpromoted it is repainted sharp at every scale */
  .pill { container-type: size; will-change: auto; }
  .pill__now { gap: 3cqw; }
  .pill__title { font-size: 11cqw; text-shadow: 0 .6cqw 6cqw rgba(0, 0, 0, .6); }
  .pill__eq { gap: 1.25cqw; height: 6.3cqw; }
  .pill__eq i { width: 1.1cqw; border-radius: .6cqw; }
}

/* Tall portrait phones: the product grid goes two across. At a third of a
   phone's width the photographs were thumbnails and "Screen Protection" broke
   over two lines; two across gives each picture room, and three rows still fit
   the pinned screen with space to spare. */
@media (max-width: 520px) and (min-height: 700px) {
  .cats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .cat { padding: 10px; gap: 8px; }
  .cat__name { font-size: .82rem; }
}

/* Short portrait phones (320x568, 360x640). */
@media (max-width: 820px) and (max-height: 700px) and (orientation: portrait) {
  /* Coming soon: both devices and both labels have to be on screen at once,
     so the photographs are sized off the height that is actually there. */
  .coming__scene { padding-block: 68px 12px; gap: 10px; }
  .coming__title { font-size: clamp(1.7rem, 9vw, 2.1rem); }
  .coming__lead  { font-size: .92rem; line-height: 1.45; }
  .coming__products { gap: 10px; }
  .product { gap: 6px; }
  .product__meta { gap: 4px; }
  .product__name { font-size: 1rem; }

  /* Spotlight: less chrome around the card, so the photograph keeps its size */
  .inside { gap: 10px; padding-bottom: 14px; }
  .inside__title { font-size: 1.55rem; }
  .spot { gap: 10px; }
  .tcard { padding: 10px; gap: 8px; }
  .tcard__text { font-size: .76rem; line-height: 1.42; }
  .tcard__frame {
    height: 30vh;
    width: min(100%, calc(30vh * 7 / 9));
  }
  .ig { padding: 8px 16px 8px 10px; }
}

/* Landscape phones: a pinned scene has 320-430px of height to work with. */
@media (orientation: landscape) and (max-height: 500px) and (max-width: 1000px) {
  .nav { padding-block: 6px; }

  /* Hero: the lockup is sized by height, so the line under it and the cue
     below that are on screen rather than under the fold */
  .origin__logo { width: min(40vw, 50vh); }
  .origin__wrap { gap: 10px; }
  .cue { bottom: 10px; gap: 6px; }
  .cue__line { height: 26px; }

  /* Coming soon: the devices sit side by side again, as they do on desktop */
  .coming__scene { padding-block: 56px 12px; gap: 10px; }
  .coming__head { gap: 6px; }
  .coming__title { font-size: 1.7rem; }
  .coming__lead { font-size: .88rem; line-height: 1.4; }
  .coming__products { grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-rows: minmax(0, 1fr); gap: 24px; align-items: stretch; height: 100%; }
  .product { gap: 6px; height: 100%; }
  .product__name { font-size: 1rem; }
  .product__shot { height: 100%; }
  .product__shot img { position: absolute; inset: 0; width: 100%; height: 100%; max-height: none; }

  /* The lens and the heading above it */
  .lens { padding-top: 62px; padding-bottom: 12px; gap: 8px; }
  .lens__title { font-size: 1.6rem; }

  /* The product grid runs six across in a single row */
  .explore { padding-block: 54px 12px; gap: 10px; }
  .explore__title { font-size: 1.5rem; }
  .cats { grid-template-columns: repeat(6, minmax(0, 1fr)); }

  /* Spotlight: heading, controls and the Instagram link to the left, the
     cards to the right — the only arrangement where the photographs still
     have height to show in */
  .inside {
    grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
    grid-template-rows: auto auto;
    align-content: center;
    column-gap: 22px;
    row-gap: 10px;
    padding: 56px 0 10px;
  }
  .inside__head { grid-column: 1; grid-row: 1; justify-items: start; text-align: left; }
  .inside__title { font-size: 1.5rem; }
  .spot { grid-column: 2; grid-row: 1 / span 2; width: 100%; align-self: center; }
  .ig { grid-column: 1; grid-row: 2; justify-self: start; padding: 8px 14px 8px 10px; }
  .spot { --spot-n: 2; }
  .tcard { padding: 8px; gap: 6px; }
  .tcard__text { font-size: .72rem; line-height: 1.4; }
  .tcard__frame {
    height: 38vh;
    width: min(100%, calc(38vh * 7 / 9));
  }

  /* Services and the footer are ordinary scrolling content, so they only
     need to use the width a landscape phone has */
  .deeds { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .foot__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ═══════════════════════════════════════════════ reduced motion ═════════ */
/* The lens journey is a sequence: three layers share one stage and only one of
   them is visible at a time, which works because the timeline says so. With
   the timeline gone there is nothing to say it, so the stage stops overlapping
   and reads top to bottom instead — the photograph, its copy, then the
   categories. The passage and the optical rig are motion, so they step out. */
@media (prefers-reduced-motion: reduce) {
  /* Both of these scenes stack several layers on one stage and show one at a
     time because a timeline says so. With the timeline gone there is nothing to
     say it, so the stages stop overlapping and read top to bottom instead. */
  #camera.pinned, #island.pinned { height: auto; }
  #camera .stage, #island .stage { height: auto; display: block; overflow: visible; }

  .lens,
  .camera__shot,
  .island__shot,
  .explore {
    position: static;
    translate: none;
    width: auto;
    opacity: 1 !important;
    transform: none !important;
  }
  .lens { display: block; padding: clamp(48px, 8vh, 96px) var(--gutter); }
  .lens__head { margin-bottom: clamp(22px, 4vh, 44px); }
  .camera__shot { width: min(94vw, 880px); margin-inline: auto; }
  .island__shot img { height: auto; object-fit: contain; }

  .explore  { padding: clamp(48px, 8vh, 96px) var(--gutter); }
  .cats .cat, .deed, .tcard, .ig { opacity: 1 !important; transform: none !important; }
  /* the carousel becomes a plain scrolling row: no paging, no transform */
  .spot__viewport { overflow-x: auto; }
  .spot__controls { display: none; }
  .inside { position: static; translate: none; width: auto; opacity: 1 !important; }

  /* the passage, the aperture and the island overlay are motion, so they go */
  .optic, .portal, .iris, .pill { display: none; }

  /* the field holds still rather than drifting (js/field.js paints one frame) */
  .field { opacity: 1; }
}
