/* ============================================================
   PXL Pixel Grid component — used in:
     · Homepage hero (right side)
     · /services hero (right side)
     · /about/pxl-design hero (right side)
     · First-load intro overlay (centre, then docks to home hero)
     · Page-transition overlay (centre, gather → disperse)

   Five visual states, all driven by `data-state` on the host:
     loading, idle, disperse, assemble, static

   The grid maintains its formation rotation throughout idle/loading;
   individual pixels carry a per-pixel ripple. Disperse + assemble
   use --dx/--dy/--rot CSS custom properties (set inline by the JS
   class) so each pixel scatters along its own deterministic vector.
   ============================================================ */

.pxl-grid {
  --pg-size: 7;
  --pg-px: 36px;
  --pg-gap: 6px;
  /* Computed dimension — re-derived whenever --pg-px / --pg-gap /
     --pg-size change. This means a @media override of --pg-px on
     mobile resizes the whole grid cleanly without needing the JS
     to re-set anything. */
  --pg-dim: calc(var(--pg-size) * var(--pg-px) + (var(--pg-size) - 1) * var(--pg-gap));
  display: grid;
  grid-template-columns: repeat(var(--pg-size), var(--pg-px));
  grid-template-rows:    repeat(var(--pg-size), var(--pg-px));
  gap: var(--pg-gap);
  width: var(--pg-dim);
  height: var(--pg-dim);
  position: relative;
  --pg-rot-dur: 60s;
  transform-origin: center center;
}

/* ---------- Per-pixel base ----------------------------------- */
.pxl-pixel {
  /* Both width AND min-width set so the cell holds its size even
     under tight grid auto-tracks (Mobile Safari can collapse cells
     to 0 if the grid track resolves before the inline --pg-px is
     applied). */
  width: var(--pg-px, 36px);
  height: var(--pg-px, 36px);
  min-width: var(--pg-px, 36px);
  min-height: var(--pg-px, 36px);
  border-radius: 2px;
  background: #888;
  position: relative;
  transition: opacity .5s ease;
}
.pxl-pixel--b { background: #5B5AFF; }
.pxl-pixel--r { background: #E8453C; }
.pxl-pixel--c { background: #F5F0DC; }
/* Empty slot — still rendered for layout integrity (so the 7×7
   grid doesn't reflow when a pattern has gaps), but transparent.
   On shape switch the assemble keyframe handles the visual
   transition from old pattern to new. */
.pxl-pixel--_ { background: transparent; }

/* Mono variant — flat greys instead of brand colours. Tuned for the
   /work hero which sits on a cream background; previous values had
   white pixels (.pxl-pixel--r) that disappeared on the light surface.
   The new palette is dark / mid / light grey so every shade reads
   on both light and dark backgrounds. */
.pxl-grid--mono .pxl-pixel--b { background: #1f1f1f; }  /* near-ink */
.pxl-grid--mono .pxl-pixel--r { background: #555555; }  /* mid grey */
.pxl-grid--mono .pxl-pixel--c { background: #888888; }  /* light grey */
.pxl-grid--mono .pxl-pixel--b.is-shot::before,
.pxl-grid--mono .pxl-pixel--b.is-shot::after { background: #1f1f1f; }
.pxl-grid--mono .pxl-pixel--r.is-shot::before,
.pxl-grid--mono .pxl-pixel--r.is-shot::after { background: #555555; }
.pxl-grid--mono .pxl-pixel--c.is-shot::before,
.pxl-grid--mono .pxl-pixel--c.is-shot::after { background: #888888; }

/* ---------- IDLE / LOADING -----------------------------------
   Slow whole-grid rotation + per-pixel ripple. The two states are
   visually identical per the spec; loading is set during page
   transitions while the grid is the only thing on screen. */
.pxl-grid[data-state="idle"],
.pxl-grid[data-state="loading"] {
  animation: pxl-grid-rotate var(--pg-rot-dur) linear infinite;
}
.pxl-grid[data-state="idle"]    .pxl-pixel,
.pxl-grid[data-state="loading"] .pxl-pixel {
  animation: pxl-pixel-ripple 4.5s ease-in-out infinite;
  /* Stagger the ripple so it travels diagonally across the grid. */
  animation-delay: calc(var(--i, 0) * 60ms);
}
@keyframes pxl-grid-rotate {
  to { transform: rotate(360deg); }
}
@keyframes pxl-pixel-ripple {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(.78); }
}

/* ---------- DISPERSE -----------------------------------------
   Pixels translate along their per-pixel vector and fade out.
   Cubic-bezier easing matches the brand's standard ease-out curve
   used everywhere else (cards lift, buttons, etc.). */
.pxl-grid[data-state="disperse"] {
  animation: none;
}
.pxl-grid[data-state="disperse"] .pxl-pixel {
  animation: pxl-pixel-disperse .9s cubic-bezier(.6, .04, .98, .34) forwards;
  animation-delay: calc(var(--i, 0) * 12ms);
}
@keyframes pxl-pixel-disperse {
  0% {
    transform: translate(0, 0) rotate(0deg) scale(1);
    opacity: 1;
  }
  100% {
    transform: translate(var(--dx, 600px), var(--dy, 0)) rotate(var(--rot, 0deg)) scale(.4);
    opacity: 0;
  }
}

/* ---------- ASSEMBLE -----------------------------------------
   Reverse of disperse — pixels fly in from their scatter position
   to formation. Backwards fill mode keeps them at the start state
   until their staggered delay fires. */
.pxl-grid[data-state="assemble"] {
  animation: none;
}
.pxl-grid[data-state="assemble"] .pxl-pixel {
  animation: pxl-pixel-assemble .85s cubic-bezier(.2, .8, .2, 1) backwards;
  /* Reverse the stagger so pixels land outer-ring first, centre last
     — reads as a "snap to formation" rather than a sweep. */
  animation-delay: calc((48 - var(--i, 0)) * 14ms);
}
@keyframes pxl-pixel-assemble {
  0% {
    transform: translate(var(--dx, 600px), var(--dy, 0)) rotate(var(--rot, 0deg)) scale(.4);
    opacity: 0;
  }
  100% {
    transform: translate(0, 0) rotate(0deg) scale(1);
    opacity: 1;
  }
}

/* ---------- STATIC -------------------------------------------
   No animation. Honoured implicitly under prefers-reduced-motion,
   but also available as an explicit state for screenshots / dev. */
.pxl-grid[data-state="static"],
.pxl-grid[data-state="static"] .pxl-pixel {
  animation: none !important;
  transform: none !important;
}

/* ---------- REPLAY (per-pixel re-assemble) -------------------
   When a pixel comes back from a shoot, only that pixel runs the
   assemble keyframe — the rest of the grid keeps rotating without
   interruption. Higher specificity than the state rules so it wins
   inside an idle/loading grid. */
.pxl-grid .pxl-pixel.is-replaying {
  animation: pxl-pixel-assemble .85s cubic-bezier(.2, .8, .2, 1) backwards !important;
}

/* ---------- SHOOT (click interaction) ------------------------
   Clicked pixel scales up briefly then disappears with a small
   particle burst. The burst is a CSS ::before pseudo with two
   small radial-gradient dots that fan outward and fade. */
.pxl-pixel.is-shot {
  /* Override any state-driven animation on this one pixel. */
  animation: pxl-pixel-shoot .45s cubic-bezier(.2, .8, .2, 1) forwards !important;
  pointer-events: none;
}
@keyframes pxl-pixel-shoot {
  0%   { transform: scale(1);   opacity: 1; }
  35%  { transform: scale(1.4); opacity: 1; }
  100% { transform: scale(.4);  opacity: 0; }
}
.pxl-pixel.is-shot::before,
.pxl-pixel.is-shot::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: currentColor;
  color: inherit;
  pointer-events: none;
  animation: pxl-burst .55s cubic-bezier(.2, .8, .2, 1) forwards;
  /* Burst dots inherit the pixel's colour through `background: currentColor`
     — set by the variant rules below. */
}
.pxl-pixel--b.is-shot::before, .pxl-pixel--b.is-shot::after { background: #5B5AFF; }
.pxl-pixel--r.is-shot::before, .pxl-pixel--r.is-shot::after { background: #E8453C; }
.pxl-pixel--c.is-shot::before, .pxl-pixel--c.is-shot::after { background: #F5F0DC; }
.pxl-pixel.is-shot::after { animation-delay: 60ms; }
@keyframes pxl-burst {
  0%   { transform: translate(-50%, -50%) translate(0, 0)   scale(1); opacity: 1; }
  100% { transform: translate(-50%, -50%) translate(40px, -28px) scale(0); opacity: 0; }
}

/* Hero placement variant — the grid sits inside a container that
   handles its own positioning. The class adds a subtle drop-shadow
   so the pixels feel like they have weight against the dark canvas. */
.pxl-grid--hero {
  /* No drop shadow — the brand colours speak for themselves. */
}

/* While an inbound page-transition overlay is on screen, suppress
   the page's data-reveal animations from auto-running underneath.
   Without this, the IntersectionObserver in app.js fires for every
   in-view element while the overlay covers them — by the time the
   overlay fades, they're already fully revealed and the user sees
   the page in its final state instead of the reveal animation.

   The page-transition.js handleInbound() removes this class right
   when the overlay starts fading, then manually re-triggers the
   reveals so they animate in alongside the fade. */
html.pxl-overlay-active [data-reveal]:not(.in) {
  opacity: 0 !important;
  transform: translateY(14px) !important;
}

/* ---------- INTRO + TRANSITION OVERLAYS ----------------------
   Shared overlay shell. Both .pxl-intro (first-visit, homepage only)
   and .pxl-transition (page-to-page) use the same chrome: full-bleed
   black backdrop, centered content, fade in/out. */
.pxl-intro,
.pxl-transition {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity .35s ease;
}
.pxl-intro { opacity: 1; pointer-events: auto; }
.pxl-transition.is-active { opacity: 1; pointer-events: auto; }
.pxl-intro.is-fading,
.pxl-transition.is-leaving { opacity: 0; }

/* Intro stage — holds the logo and the grid stacked at centre.
   Both children sit at 0,0 and animate via opacity. The grid host
   itself receives a transform during the dock-to-hero phase. */
.pxl-intro__stage {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}
.pxl-intro__logo {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  transition: opacity .5s ease;
  /* 2.0s logo run — visible for ~1.4s before the grid takes over.
     Keyframe: in 0-0.4s, hold 0.4-1.7s, out 1.7-2.0s. Matches the
     1300ms grid handover in first-load-intro.js plus a small
     trailing fade so the two layers cross-fade gracefully. */
  animation: pxl-intro-logo 2s ease forwards;
}
.pxl-intro__logo img {
  width: 140px;
  height: auto;
  display: block;
}
@keyframes pxl-intro-logo {
  0%   { opacity: 0; transform: translateY(8px); }
  20%  { opacity: 1; transform: translateY(0); }
  85%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-4px); }
}

.pxl-intro__grid {
  /* Grid lives inside an additional wrapper so the dock-to-hero
     transform can be applied to the wrapper without fighting the
     pixel-grid rotation animation on the inner element. */
  position: relative;
  opacity: 0;
  transition: transform 1s cubic-bezier(.2, .8, .2, 1),
              opacity .4s ease;
}
.pxl-intro.is-grid .pxl-intro__grid { opacity: 1; }
.pxl-intro.is-fading .pxl-intro__grid { opacity: 0; }

/* Transition overlay grid sits at centre, backdrop fades in/out.
   When destination has a hero grid (homepage, /services, /about/pxl-
   design), the page-transition.js attaches `.is-relocating` to slide
   the grid up + right toward where the destination's hero grid will
   sit, instead of dispersing. Fades alongside the backdrop. */
.pxl-transition__grid {
  position: relative;
  transition: transform .8s cubic-bezier(.2, .8, .2, 1),
              opacity .5s ease;
}
.pxl-transition__grid.is-relocating {
  transform: translate(28vw, -22vh) scale(.55);
  opacity: 0;
}
@media (max-width: 768px) {
  .pxl-transition__grid.is-relocating {
    transform: translate(0, -28vh) scale(.7);
  }
}

/* ---------- HERO PIXEL GRID PLACEMENT ------------------------
   Three pages mount a hero pixel grid. The shared host is .hero-pxl
   with a known structure:
     <div class="hero-pxl" aria-hidden="true">
       <div class="hero-pxl__grid"></div>
       <div class="hero-pxl__chip">● CLICK PIXELS TO SHOOT · R TO REPLAY</div>
     </div>
   The .hero-pxl__grid is the actual PixelGrid host. The chip sits
   beneath it and uses the same mono treatment as the rest of the
   site's eyebrows. */
.hero-pxl {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Anchor for the absolutely-positioned keys hint below — keeping
     keys out of flex flow means .hero-pxl is exactly the size of the
     grid, so when the parent column applies `align-items: center` the
     grid (not the grid+keys composite) is the element centred against
     the text. Same vertical relationship between grid and text on
     every hero. */
  position: relative;
  /* Grid initially hidden on first homepage load — the FirstLoadIntro
     reveals it once the dock animation completes. Subsequent visits
     within the same session show it immediately. */
  opacity: 1;
  transition: opacity .4s ease;
}
.hero-pxl.is-pending { opacity: 0; }
.hero-pxl.is-revealed { opacity: 1; }
.hero-pxl__grid {
  /* Constrain the host so the grid never exceeds its slot. The
     PixelGrid class sets exact width/height via --pg-dim which wins. */
  display: inline-grid;
}
.hero-pxl__chip {
  /* Legacy chip — only kept around as a no-op in case any other
     hero placement still references it. The homepage hero now uses
     .hero-pxl__keys (kbd treatment) instead and the other heroes
     drop the chip entirely. */
  display: none;
}

/* Keyboard-shortcut hints under the homepage hero pixel grid.
   Mirrors the small kbd-key treatment used elsewhere on the site
   for keyboard shortcuts. Hidden on mobile via the shared mobile
   rules below. Absolutely positioned so it doesn't add height to
   .hero-pxl — that lets the grid itself be the visually-centred
   element against the hero text. `flex-wrap: nowrap` so all the
   shortcuts sit on a single horizontal line under the grid; the
   absolute box auto-sizes to its content so it never wraps. */
.hero-pxl__keys {
  position: absolute;
  top: calc(100% + 18px);
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  flex-wrap: nowrap;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
}
.hero-pxl__keys kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 22px;
  height: 22px;
  padding: 0 6px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: #151515;
  color: #ddd;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: .04em;
  text-transform: none;
  margin-right: 4px;
}
.hero-pxl__keys-sep {
  opacity: .55;
  margin: 0 2px;
}

/* ---------- MOBILE -------------------------------------------
   The hero pixel grids stay visible on mobile but in a stripped-back
   form: a single page-specific shape that just rotates and ripples on
   loop — no shape cycling, no scatter, no click/keyboard interaction
   (the per-page JS passes `interactive: false`, `shapeAuto: 0`, and a
   one-element `shapes: [...]` array). The visual sits above the
   headline (each page's hero CSS reorders so the pixel column comes
   first on mobile). The keys hint is dropped because there's no
   keyboard. */
@media (max-width: 768px) {
  /* Compact pixel size for mobile — 7×7 at 22px + 3px gap = ~172px
     wide so the grid fits comfortably above the headline at 375px
     viewport without crowding the type. Per-grid JS still passes
     `pixelSize: 22, gap: 3` on mobile so the values match. */
  .hero-pxl .pxl-grid {
    --pg-px: 22px;
    --pg-gap: 3px;
  }
  /* Keyboard hints hidden on mobile (touch — no keyboard). */
  .hero-pxl__keys { display: none; }
  /* Overlay grids (intro + transition) keep working on mobile.
     Force a smaller pixel size so they fit phone screens nicely;
     the calc-based --pg-dim recomputes automatically. */
  .pxl-intro__grid .pxl-grid,
  .pxl-transition__grid .pxl-grid {
    --pg-px: 26px !important;
    --pg-gap: 4px !important;
  }
  .pxl-intro__logo img { width: 96px; }
}

/* ---------- REDUCED MOTION -----------------------------------
   Replace all motion with simple instant fades. Static grid only. */
@media (prefers-reduced-motion: reduce) {
  .pxl-grid,
  .pxl-grid .pxl-pixel,
  .pxl-pixel.is-shot,
  .pxl-intro__logo,
  .pxl-intro__grid {
    animation: none !important;
    transition: opacity .2s ease !important;
  }
  .pxl-pixel.is-shot { opacity: 0 !important; }
  .pxl-pixel.is-shot::before,
  .pxl-pixel.is-shot::after { display: none; }
  .pxl-grid[data-state="disperse"] .pxl-pixel { opacity: 0; }
  .pxl-grid[data-state="assemble"] .pxl-pixel { opacity: 1; }
}
