/* ============================================================
   background.css — ambient background for expri.ai

   Loaded after the styles in index.html, so everything here wins.
   Three stacked layers, all decorative and non-interactive:

     .bloom          light in haze (the "ethereal" part)
     .constellation  hub-and-spoke network, static geometry
     .grain          fine noise, kills gradient banding

   MOOD DIAL: the four values in :root below control everything.
   Raise --bloom-warm-core for more light, --net-ink for a more
   present network. To remove the background entirely, empty this
   file; the page falls back to a flat surface and still works.
   ============================================================ */

:root {
  /* Slightly deeper than the flat page so the blooms have room to read. */
  --ink: #08090B;

  /* Warm light, borrowed from the fresco palette rather than the fresco.
     0.22 over #08090B lands near RGB 57 at the core: visible as light,
     still well below anything that competes with the type. */
  --bloom-warm-core: 0.22;
  /* Counterweight bloom. Followed the accent to vermillion; it is only
     visible in the non-photo fallback mode. */
  --bloom-cool-core: 0.12;

  --net-ink: rgba(243, 242, 239, 0.55);
  --grain-opacity: 0.038;

  /* Photo mode only. */
  --photo-opacity: 1;
  /* Even veil over the whole painting, replacing the old directional
     blackout. 0.28 is the lightest setting where every text zone still
     clears WCAG AA against the image's bright centre, measured against
     the actual pixels. The painting is not symmetric, so this is
     orientation-specific: 0.28 passes mirrored, but fails the hero
     paragraph by a hair in the current orientation. 0.32 because the
     glow below is additive light under the same text and has to clear
     AA at its PEAK, not just at rest. */
  --photo-veil: 0.32;
}

/* Content sits above the ambient layers. */
.shell {
  position: relative;
  z-index: 1;
}

.atmosphere {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
}

/* ============================================================
   PHOTO MODE  —  body class="bg-photo"
   Uses the painted background instead of the CSS blooms. Drop the
   class from <body> in index.html to switch back.
   ============================================================ */

.photo,
.scrim { display: none; }

body.bg-photo .photo,
body.bg-photo .scrim { display: block; }

/* In photo mode the painting supplies its own light and its own
   network, so the CSS versions come off. Running both is noise. */
body.bg-photo .bloom,
body.bg-photo .constellation { display: none; }

.photo {
  position: absolute;
  inset: 0;
  background: url('/assets/bg-wide.webp') center / cover no-repeat;
  opacity: var(--photo-opacity);
}

/* Optional mirror. Applied to the whole ambient stack so the star
   overlay and the glow mirror along with the painting and stay
   aligned. Costs nothing and needs no second asset. */
body.bg-photo.flip .atmosphere { transform: scaleX(-1); }

.plate { position: absolute; inset: 0; }

/* ---- drift ----
   A very slow parallax breath. 0.35% of viewport width is about 7px
   either side of centre on a laptop, over 75 seconds, which is under
   the threshold where the eye tracks it as motion. The 1.04 scale
   exists purely to give the translate somewhere to go without
   exposing an edge. */
.plate {
  animation: plate-drift 75s ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes plate-drift {
  from { transform: scale(1.04) translate3d( 0.35%,  0.22%, 0); }
  to   { transform: scale(1.04) translate3d(-0.35%, -0.22%, 0); }
}

/* ---- twinkle ----
   Circle positions were detected from the image itself, so these sit
   on stars that are already painted rather than floating above them. */
.twinkle {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.stars circle {
  animation-name: star-twinkle;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  opacity: 0.5;
}
@keyframes star-twinkle {
  0%, 100% { opacity: 0.22; }
  50%      { opacity: 0.85; }
}

/* ---- glow ----
   Centred on the painting's own light source (x 47%, y 50%). It sits
   inside .plate, below the scrim, so it is veiled with everything else
   and reads as the haze brightening rather than as an overlay. */
.glow {
  position: absolute;
  inset: 0;
  background: radial-gradient(
    40% 48% at 47% 50%,
    rgba(255, 238, 210, 0.045) 0%,
    rgba(255, 234, 200, 0.018) 45%,
    transparent 72%
  );
  opacity: 0.35;
  animation: glow-breathe 14s ease-in-out infinite;
  transition: opacity 900ms cubic-bezier(0.22, 1, 0.36, 1);
}
@keyframes glow-breathe {
  0%, 100% { opacity: 0.30; }
  50%      { opacity: 1.00; }
}

/* Hovering any product lifts the light. There is no spatial mapping
   between a product and a region of the painting, so brightening a
   "corresponding" area would be arbitrary; the whole field responding
   is honest and reads as the page waking up. */
body.bg-photo:has(.row:hover, .variant:hover) .glow {
  animation-play-state: paused;
  opacity: 1;
}

/* No directional blackout. The old version drove the left 58% of the
   viewport to solid ink, which hid the painting entirely on the side
   you actually look at first. Instead: one light even veil, with the
   top and bottom edges settled so the masthead and colophon are not
   floating on mid-tone haze. Text legibility is handled on the TEXT
   now (brighter greys + a halo) rather than by hiding the image. */
.scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    rgba(8, 9, 11, 0.88) 0%,
    rgba(8, 9, 11, var(--photo-veil)) 15%,
    rgba(8, 9, 11, var(--photo-veil)) 78%,
    rgba(8, 9, 11, 0.90) 100%
  );
}

/* ---- photo-mode typography ----
   Low-contrast greys are a dark-flat-surface luxury. Over a painting
   whose centre hits luminance 123, they fall to ~2.7:1 and become
   unreadable, so the secondary text steps up and hierarchy is carried
   by size and weight instead of by how dim the colour is. */
body.bg-photo {
  --muted:      #D5D4D0;
  --muted-soft: #BFBEBA;
}

/* A tight dark halo behind the glyphs. This buys real local contrast
   over the bright areas without darkening a single pixel that is not
   directly behind text. */
body.bg-photo .shell {
  text-shadow:
    0 1px 2px rgba(8, 9, 11, 0.55),
    0 2px 14px rgba(8, 9, 11, 0.75);
}

/* ---------- 1. light in haze ----------
   A bright-ish core with a long tail, rather than one flat wash.
   The core is what reads as a light source; the tail is what reads
   as haze. No filter: blur() here on purpose. The gradient is
   already smooth, and blurring a 1300px element just flattens the
   core back out while costing a full-screen GPU pass. */

.bloom {
  position: absolute;
  border-radius: 50%;
}

.bloom-warm {
  width: 90vmax;
  height: 90vmax;
  top: -28vmax;
  right: -16vmax;
  background: radial-gradient(
    closest-side,
    rgba(255, 240, 220, var(--bloom-warm-core)) 0%,
    rgba(255, 236, 212, 0.130) 22%,
    rgba(255, 233, 205, 0.060) 44%,
    rgba(255, 230, 200, 0.020) 64%,
    transparent 80%
  );
}

.bloom-cool {
  width: 80vmax;
  height: 80vmax;
  bottom: -34vmax;
  left: -20vmax;
  background: radial-gradient(
    closest-side,
    rgba(255, 90, 80, var(--bloom-cool-core)) 0%,
    rgba(75, 214, 226, 0.055) 26%,
    rgba(75, 210, 224, 0.020) 50%,
    transparent 74%
  );
}

/* ---------- 2. the network ----------
   Weighted to the right so it occupies the space the left-aligned
   copy leaves empty. Static geometry: no physics, no mouse
   tracking, no drifting particles. */

.constellation {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  color: var(--net-ink);
}

.constellation .links {
  opacity: 0.30;
  vector-effect: non-scaling-stroke;
}

.constellation .dots { opacity: 0.50; }
.constellation .hub  { opacity: 0.80; }

/* One node per product: the only accent in the field. */
.constellation .nodes {
  color: var(--accent);
  opacity: 0.95;
}

/* A slow, staggered breath. Long enough that it reads as ambient
   rather than as an animation you are meant to watch. */
.constellation .nodes circle {
  animation: node-breathe 11s ease-in-out infinite;
  transform-box: fill-box;
  transform-origin: center;
}
.constellation .n2 { animation-delay: -3.7s; }
.constellation .n3 { animation-delay: -7.4s; }
.constellation .n4 { animation-delay: -2.2s; }
.constellation .n5 { animation-delay: -9.1s; }
.constellation .n6 { animation-delay: -5.5s; }

@keyframes node-breathe {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1;    }
}

/* ---------- 3. grain ----------
   Large smooth gradients band into visible rings on 8-bit displays.
   Noise dithers the steps away and adds a bit of tooth. */

.grain {
  position: absolute;
  inset: -50%;
  opacity: var(--grain-opacity);
  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='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---------- narrow screens ----------
   The network is tuned for a wide viewport; on a phone it crops to
   a meaningless corner of itself, so drop it and keep the light. */

@media (max-width: 720px) {
  .constellation { display: none; }
  .bloom-warm { top: -40vmax; right: -28vmax; }

  /* THE PAINTING IS ROTATED FOR PORTRAIT, NOT CROPPED.
     The composition is a hand-to-hand reach across a LANDSCAPE frame —
     human on the left, the wireframe AI hand on the right, the glow
     between the fingertips. Any portrait crop of that either loses a
     hand or shoves both to the edges. Turned 90 degrees clockwise the
     same gesture becomes vertical and fits a phone exactly: the human
     hand reaches DOWN from the top, the AI hand reaches UP from the
     bottom, and they meet at the glow in the middle — which is also
     the right way round for the argument the site makes.
     bg-portrait.webp is bg-wide.webp rotated, not a new painting, so it
     costs 18 KB and no new artwork. bg-tall.webp is left in the repo
     unused; it was the old crop. */
  .photo { background-image: url('/assets/bg-portrait.webp'); }

  /* THE STARS COME OFF ON MOBILE. .twinkle's viewBox is 1536x1024 —
     the WIDE image's own pixel dimensions — which is the only reason
     its dots land on painted stars. Against a rotated background the
     alignment is meaningless. It was already meaningless against
     bg-tall.webp; the 0.94 scrim just hid it. */
  .twinkle { display: none; }

  /* 0.62, DOWN FROM 0.80-0.94, and the old value is why Peter could not
     see the painting on a phone at all — up to 94% black over it.
     Landscape looked fine only because a phone in landscape is wider
     than 720px, so none of this applied.
     The number is measured, not taste: at veil 0.32 the desktop accepts
     3.99:1 for --muted over the median of bg-wide. Reproducing that same
     contrast over the portrait crop needs 0.54. 0.62 keeps a margin over
     that, and still lets roughly three times more of the painting
     through than 0.94 did.
     Flat-ish rather than directional: on a phone the copy spans the full
     width, so there is no clear side to protect — only the masthead and
     the colophon need their own cover. */
  .scrim {
    background: linear-gradient(
      to bottom,
      rgba(8, 9, 11, 0.78) 0%,
      rgba(8, 9, 11, 0.60) 22%,
      rgba(8, 9, 11, 0.62) 72%,
      rgba(8, 9, 11, 0.80) 100%
    );
  }
}

@media (prefers-reduced-motion: reduce) {
  .constellation .nodes circle { animation: none; opacity: 0.8; }
  /* Everything ambient holds still. The glow keeps its mid value so
     the page looks the same as a paused frame, not a different design. */
  .plate        { animation: none; transform: scale(1.04); }
  .stars circle { animation: none; opacity: 0.55; }
  .glow         { animation: none; opacity: 0.65; }
}
