/* ===========================================================================
   GLEAM & GLOW — THE LIGHT LAYER
   ---------------------------------------------------------------------------
   Everything in this file is about one thing: light moving across a clean
   surface. That is the product, it is the name, and it is the only special
   effect this site is allowed.

   WHAT THIS IS NOT. It is not neon, it is not a starfield, and there is not a
   single square glowing panel in it. The brief was "make it feel like 3026",
   and the ten-a-penny version of that is cyan light on black — which would
   throw away a blush-and-serif identity sampled from her own business card and
   leave her looking like a crypto exchange. The expensive-looking future is
   depth, material and slow motion: light that pools, drifts, catches an edge
   and passes on. That is what is in here.

   IT IS ADDITIVE, DELIBERATELY. styles.css alone renders the whole site
   correctly. Delete the two <link>/<script> lines for this layer and nothing
   breaks, nothing is left half-styled, and no layout moves. Every rule below
   either adds a light source or lifts an existing element — none of them are
   load-bearing.

   THE THREE BUDGETS, all of which this stays inside:

     MOTION      Every animation here is 8 seconds or slower, except the ones
                 the pointer drives. Nothing flashes, nothing strobes, and the
                 whole file is switched off under prefers-reduced-motion.

     PAINT       No filter: blur() on anything large — a 60px blur across a
                 full-screen element is the single most reliable way to drop a
                 phone to 12fps. Soft edges come from radial-gradient stops,
                 which the compositor gets for free.

     ATTENTION   Nothing in here competes with a photograph or with the
                 WhatsApp button. If an effect draws the eye away from her
                 work or away from the way to contact her, it is wrong,
                 however good it looks.
   ========================================================================= */

/* =========================================================================
   1 — THE LIGHT FIELD
   -------------------------------------------------------------------------
   Three soft pools of colour drifting behind the whole page, at speeds slow
   enough that you never catch them moving — you only notice that the cream
   is not flat.

   Built from radial-gradients on transparent elements rather than from
   blurred shapes. Same look, none of the cost: a blur filter forces the
   compositor to re-rasterise every frame, and this is on screen for the
   entire visit.
   ========================================================================= */

.lightfield {
  position: fixed;
  /* Oversized so a drifting pool never shows its own edge at the viewport. */
  inset: -25vmax;
  z-index: 0;
  pointer-events: none;
  contain: strict;
}

.lightfield i {
  position: absolute;
  display: block;
  border-radius: 50%;
  will-change: transform;
}

.lightfield i:nth-child(1) {
  width: 70vmax; height: 70vmax;
  top: -8%; left: -10%;
  background: radial-gradient(circle at 50% 50%,
              rgba(207, 164, 177, .40), rgba(207, 164, 177, 0) 68%);
  animation: lf-a 54s var(--ease-soft) infinite alternate;
}
.lightfield i:nth-child(2) {
  width: 58vmax; height: 58vmax;
  top: 34%; right: -14%;
  background: radial-gradient(circle at 50% 50%,
              rgba(138, 77, 94, .22), rgba(138, 77, 94, 0) 66%);
  animation: lf-b 71s var(--ease-soft) infinite alternate;
}
.lightfield i:nth-child(3) {
  width: 46vmax; height: 46vmax;
  bottom: -6%; left: 22%;
  background: radial-gradient(circle at 50% 50%,
              rgba(231, 208, 214, .55), rgba(231, 208, 214, 0) 70%);
  animation: lf-c 63s var(--ease-soft) infinite alternate;
}

@keyframes lf-a {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(14vmax, 9vmax, 0) scale(1.18); }
}
@keyframes lf-b {
  from { transform: translate3d(0, 0, 0) scale(1.1); }
  to   { transform: translate3d(-12vmax, -14vmax, 0) scale(.92); }
}
@keyframes lf-c {
  from { transform: translate3d(0, 0, 0) scale(.95); }
  to   { transform: translate3d(10vmax, -11vmax, 0) scale(1.22); }
}

/* For the field to be visible at all, the page above it has to stop being an
   opaque sheet of cream. The body colour stays as the floor — nothing ever
   renders on nothing — and the sections that used to paint over it become
   translucent instead. */
body { background: var(--cream); }

/* ⚠  ONLY the elements that are `position: static` today are listed here.
   The first version of this rule also named .header and .mobile-bar, which
   are `sticky` and `fixed` respectively — setting `position: relative` on
   them silently unstuck the navigation and dropped the phone's WhatsApp bar
   into the bottom of the document. Both already carry their own position and
   their own z-index (50 and 90), so they sit above the light field without
   any help from here. */
main, .footer, .marquee { position: relative; z-index: 1; }
.bg-warm { background: rgba(244, 236, 234, .72); }
.marquee { background: rgba(249, 245, 244, .8); }

/* =========================================================================
   2 — GRAIN
   -------------------------------------------------------------------------
   A single tile of fractal noise over everything, at 3.5%. It is the cheapest
   trick in the file and the one that does most: large flat gradients band
   visibly on an 8-bit screen, and a whisper of grain breaks the banding up
   and reads as paper rather than as PNG.
   ========================================================================= */

.grain {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: .035;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
  /* Fixed, and NOT animated. Animated grain is film; static grain is paper.
     A cleaning company should look like the second one. */
}

/* =========================================================================
   3 — THE POINTER HALO
   -------------------------------------------------------------------------
   A pool of warm light that follows the cursor around the page, lagging
   behind it. It is not a cursor replacement — the real cursor is untouched,
   because taking somebody's pointer away to make a website look clever is a
   bad trade.

   Fine pointers only. On a touch screen there is no pointer to follow, and
   the element would sit in one corner glowing at nothing.
   ========================================================================= */

.halo {
  position: fixed;
  top: 0;
  left: 0;
  width: 34rem;
  height: 34rem;
  margin: -17rem 0 0 -17rem;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(circle at 50% 50%,
              rgba(207, 164, 177, .34), rgba(207, 164, 177, .10) 42%,
              rgba(207, 164, 177, 0) 70%);
  transition: opacity .9s var(--ease-soft);
  will-change: transform;
}
.has-halo .halo { opacity: 1; }

@media (hover: none), (pointer: coarse) {
  .halo { display: none; }
}

/* =========================================================================
   4 — LIT SURFACES
   -------------------------------------------------------------------------
   The pointer-driven highlight that turns a flat card into something with a
   surface. --mx and --my are set per element by glow.js as percentages, and
   the highlight is drawn where the pointer is.

   `--lit: 0` when the pointer is elsewhere, `1` when it is over the element,
   and the transition between them is what stops this reading as a hover
   state and starts it reading as a light being moved across a thing.
   ========================================================================= */

.lit {
  --mx: 50%;
  --my: 50%;
  --lit: 0;
  position: relative;
  isolation: isolate;
}
.lit::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  border-radius: inherit;
  opacity: var(--lit);
  transition: opacity .55s var(--ease-soft);
  background: radial-gradient(
    18rem 18rem at var(--mx) var(--my),
    rgba(255, 255, 255, .40), rgba(255, 255, 255, .10) 38%,
    rgba(255, 255, 255, 0) 68%
  );
}

/* Over a photograph the same highlight has to be gentler, or it fogs the
   picture — which on a site whose entire argument is "look at my work" is the
   one thing it must not do. */
.figure.lit::before,
.gallery__item.lit::before {
  background: radial-gradient(
    14rem 14rem at var(--mx) var(--my),
    rgba(255, 255, 255, .26), rgba(255, 255, 255, 0) 62%
  );
}

/* The lit edge. A hairline that brightens where the pointer is nearest,
   which is what sells a surface as having a physical edge. */
.lit-edge {
  position: relative;
}
.lit-edge::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  border-radius: inherit;
  padding: 1px;
  background: radial-gradient(
    22rem 22rem at var(--mx) var(--my),
    rgba(255, 255, 255, .85), rgba(207, 164, 177, .35) 40%,
    rgba(207, 164, 177, 0) 70%
  );
  /* The standard two-layer mask that leaves only the border box painted. */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
  opacity: calc(var(--lit) * .9);
  transition: opacity .55s var(--ease-soft);
}

/* =========================================================================
   5 — TILT
   -------------------------------------------------------------------------
   Cards lean a couple of degrees towards the pointer. Two degrees, not ten:
   the effect should be felt and not seen, and anything past about three
   starts distorting the photograph inside it.
   ========================================================================= */

.tilt {
  --rx: 0deg;
  --ry: 0deg;
  transform: perspective(1200px) rotateX(var(--rx)) rotateY(var(--ry));
  transform-style: preserve-3d;
  transition: transform .8s var(--ease);
}
.tilt.is-tilting { transition: transform .12s linear; }

/* =========================================================================
   6 — MAGNETIC BUTTONS
   -------------------------------------------------------------------------
   The button leans towards an approaching cursor. It is the oldest trick in
   the "expensive website" book and it still works, as long as the pull is
   small enough that the button never moves out from under the click.
   ========================================================================= */

.btn {
  --pull-x: 0px;
  --pull-y: 0px;
}
.btn.is-magnetic {
  transform: translate3d(var(--pull-x), var(--pull-y), 0);
  transition: transform .5s var(--ease), box-shadow .5s var(--ease);
}
/* The base rule lifts the button 2px on hover with its own transform, which
   would otherwise cancel the magnet. Fold the lift into the same transform. */
.btn.is-magnetic:hover,
.btn.is-magnetic:focus-visible {
  transform: translate3d(var(--pull-x), calc(var(--pull-y) - 2px), 0);
}

/* =========================================================================
   7 — THE HERO
   ========================================================================= */

/* A shaft of light crossing the photograph, once every twenty seconds. The
   hero already has the .gleam sweep on it; this is slower, wider and much
   fainter, and the two crossing at different rates is what stops the loop
   being noticeable as a loop. */
.hero__media::before {
  content: '';
  position: absolute;
  inset: -30% -60%;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(104deg,
    transparent 0%, rgba(255, 255, 255, 0) 38%,
    rgba(255, 255, 255, .16) 48%, rgba(255, 255, 255, .22) 51%,
    rgba(255, 255, 255, 0) 62%, transparent 100%);
  animation: shaft 21s var(--ease-soft) infinite;
}
@keyframes shaft {
  0%, 12%  { transform: translateX(-38%); opacity: 0; }
  30%      { opacity: 1; }
  62%      { opacity: 1; }
  80%, 100%{ transform: translateX(38%); opacity: 0; }
}
/* On a phone the shaft is the same width as most of the photograph, so at
   full strength it stops reading as light falling across a bathroom and
   starts reading as a bright stripe over her work. Halved below the split. */
@media (max-width: 700px) {
  .hero__media::before { opacity: .5; }
}

/* The headline glows very slightly, and breathes. At .18 opacity on a
   text-shadow this is below the threshold of "an effect" and above the
   threshold of "this type is lit from behind". */
.hero__title {
  animation: title-breathe 9s var(--ease-soft) infinite alternate;
}
@keyframes title-breathe {
  from { text-shadow: 0 2px 40px rgba(38, 24, 29, .45),
                      0 0 30px rgba(231, 208, 214, .10); }
  to   { text-shadow: 0 2px 40px rgba(38, 24, 29, .45),
                      0 0 60px rgba(231, 208, 214, .30); }
}

/* =========================================================================
   8 — THE SHIMMER
   -------------------------------------------------------------------------
   A band of light travelling once through a heading as it arrives. Applied to
   the accent half of a two-tone heading only — running it through a whole
   paragraph of display type turns a nice moment into a slot machine.
   ========================================================================= */

.shimmer {
  background: linear-gradient(100deg,
    currentColor 0%, currentColor 38%,
    rgba(255, 255, 255, .95) 50%,
    currentColor 62%, currentColor 100%);
  background-size: 260% 100%;
  background-position: 130% 0;
  -webkit-background-clip: text;
          background-clip: text;
  /* The text itself has to become transparent for the gradient to show, and
     that has one consequence worth knowing: `color` now only feeds
     currentColor in the gradient above, so the heading's colour still comes
     from the same place it always did. */
  -webkit-text-fill-color: transparent;
          color: transparent;
}
.is-in .shimmer,
.shimmer.is-in { animation: shimmer 2.4s var(--ease-soft) .35s forwards; }
@keyframes shimmer {
  to { background-position: -30% 0; }
}

/* =========================================================================
   9 — THE DARK BANDS
   -------------------------------------------------------------------------
   The rose "how it works" band and the quote panel's aside are the two places
   where light can actually be seen doing something, because they are the only
   dark surfaces on the site. They get a slow aurora.
   ========================================================================= */

/* THE STACKING HERE IS DELIBERATE and was got wrong once. The aurora sits at
   z-index -1 inside an isolated stacking context, so every child of the band
   paints above it without any of them needing a z-index — or, crucially, a
   `position`. The first version raised the children instead, with
   `.steps__mark { position: relative }`, which quietly cancelled the
   `position: absolute` that puts her emblem in the bottom corner and dropped
   it on top of the heading. */
.steps, .quote__aside { isolation: isolate; }

.steps::before,
.quote__aside::before {
  content: '';
  position: absolute;
  inset: -40%;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(40% 34% at 22% 18%, rgba(231, 208, 214, .30), transparent 62%),
    radial-gradient(34% 30% at 78% 72%, rgba(207, 164, 177, .26), transparent 64%),
    radial-gradient(30% 40% at 58% 30%, rgba(255, 255, 255, .10), transparent 60%);
  animation: aurora 38s var(--ease-soft) infinite alternate;
}
@keyframes aurora {
  from { transform: translate3d(-4%, -3%, 0) rotate(0deg) scale(1); }
  to   { transform: translate3d(5%, 4%, 0) rotate(8deg) scale(1.15); }
}
/* The big numerals on the steps pick up a glow, so the sequence reads as lit
   rather than as printed. */
.step__n {
  text-shadow: 0 0 34px rgba(231, 208, 214, .5);
  animation: numeral 7s var(--ease-soft) infinite alternate;
}
.step:nth-child(2) .step__n { animation-delay: -2.4s; }
.step:nth-child(3) .step__n { animation-delay: -4.8s; }
@keyframes numeral {
  from { opacity: .16; }
  to   { opacity: .34; }
}

/* =========================================================================
   10 — THE SCROLL LINE
   -------------------------------------------------------------------------
   A hairline of her rose across the very top of the window, filling as the
   page is read. It doubles as the only "you are here" the site has.
   ========================================================================= */

.progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 100;
  pointer-events: none;
  transform-origin: left;
  transform: scaleX(var(--p, 0));
  background: linear-gradient(90deg,
              var(--rose-deep), var(--rose) 40%, var(--blush) 72%, #fff);
  box-shadow: 0 0 12px rgba(138, 77, 94, .5);
}

/* =========================================================================
   11 — SCROLL-DRIVEN DRIFT
   -------------------------------------------------------------------------
   Photographs drift a little as they cross the viewport. This is the one
   effect written against `animation-timeline`, because it is the only one
   where the browser doing the work itself — off the main thread, with no
   scroll listener anywhere — is materially better than the JavaScript
   version.

   Behind @supports, so a browser without it simply gets a still photograph,
   which is what the site looked like yesterday and is perfectly fine.
   ========================================================================= */

@supports (animation-timeline: view()) {
  @media (min-width: 760px) {
    .figure--drift img,
    .about__card img {
      animation: drift linear both;
      animation-timeline: view();
      animation-range: entry 0% exit 100%;
    }
    @keyframes drift {
      from { transform: translateY(-2.5%) scale(1.06); }
      to   { transform: translateY(2.5%) scale(1.06); }
    }
  }
}

/* =========================================================================
   12 — THE MARK, LIT
   -------------------------------------------------------------------------
   The emblem in the header catches the light as the page loads and then very
   occasionally afterwards, the way a piece of foil on a business card does
   when you turn it.
   ========================================================================= */

.brand__mark {
  animation: mark-catch 14s var(--ease-soft) 2s infinite;
  transform-origin: 50% 60%;
}
@keyframes mark-catch {
  0%, 88%, 100% { filter: none; transform: none; }
  92%           { filter: brightness(1.22) saturate(1.1); transform: scale(1.06); }
}

/* =========================================================================
   REDUCED MOTION
   -------------------------------------------------------------------------
   The whole layer stops. Not "slows down" — stops. The light field freezes
   mid-drift and stays there, which is a perfectly good static background, and
   every pointer-driven effect is disabled at source in glow.js as well, so
   nothing here is relying on a CSS override alone.
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  .lightfield i,
  .hero__media::before,
  .hero__title,
  .steps::before,
  .quote__aside::before,
  .step__n,
  .brand__mark,
  .figure--drift img,
  .about__card img { animation: none !important; }

  .halo { display: none !important; }
  .tilt { transform: none !important; }
  .btn.is-magnetic { transform: none !important; }
  .shimmer {
    -webkit-text-fill-color: currentColor;
    color: inherit;
    background: none;
    animation: none !important;
  }
  .lit::before, .lit-edge::after { opacity: 0 !important; }
}

/* =========================================================================
   PRINT
   ========================================================================= */

@media print {
  .lightfield, .grain, .halo, .progress { display: none !important; }
  .lit::before, .lit-edge::after { display: none !important; }
  .shimmer { -webkit-text-fill-color: #000; color: #000; background: none; }
}
