/* Shared SPA shell — loaded on all four public-site pages (/, /work,
   /bookings, /live) so router.js can navigate between them without ever
   re-creating, re-sourcing or restarting the background <video>.

   #site-bg   — persistent. Holds the hatch/video/scrim trio. Never
                touched by a swap; router.js only ever replaces #site-view.
   #site-view — swappable. Everything else in <body> lives here. */

#site-bg {
  position: fixed;
  inset: 0;
  /* Not -1. A <video> inside a negative-z-index stacking context silently
     fails to composite on some Chromium builds (confirmed via headless
     Chrome + CDP: video reports readyState 4 / playing / currentTime
     advancing, yet paints nothing — reproduced in a 20-line isolated
     repro, fixed by removing the negative z-index). #site-bg instead
     stays behind #site-view by DOM order alone: both sit at the same
     z-index stacking level (0 here, 1 there — see #site-view below), and
     #site-bg comes first in the document, so painting order (not a
     negative index) is what keeps it behind. */
  z-index: 0;
  background: var(--bg);
}

/* The video is the permanent background of the ENTIRE site — never hidden,
   on any route. This previously read:

     body:not([data-route="home"]) #site-bg { visibility: hidden; }

   which scoped it to Home. That rule is gone deliberately. The <video> node
   already survived every navigation (router.js only ever swaps #site-view),
   so deleting the hide was all that stood between "playing but covered" and
   "the background of the whole site".

   Two consequences worth knowing:
   - matrix.css's opaque `body` background is what actually covered it on
     /work and /bookings — NOT the rain canvas, which was only opacity .12.
     See the `body[data-route]` rule there.
   - Text on those pages now sits over moving footage rather than a flat
     colour, so contrast is measured against the clip's brightest frames. */

/* ── The background layer itself ────────────────────────────────────────
   These three live here, not in home.css, because they belong to the shell
   and every shell page must style them. When they were in home.css — which
   only the enter screen loads — the <video> fell back to its intrinsic
   854x576 box in the top-left corner of /work and /bookings instead of
   covering the viewport. */

.home-hatch {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(135deg, rgba(242, 240, 233, .035) 0 9px, transparent 9px 18px);
}

.home-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* No poster/background of its own — while no source resolves, the plain
     ground + hatch behind it must show through untouched. */
  background: transparent;
}

/* Hidden by JS the moment every <source> fails to load, so a missing file
   never paints a black box or a broken-media icon. Also the fallback for
   no-JS: an unplayable video with no poster still sits inset:0, so keep it
   transparent by default rather than relying on JS alone. */
.home-video[hidden] {
  display: none;
}

/* ── Scrim ──────────────────────────────────────────────────────────────
   Tuned against the real clip (bright sunlit water over pale stones). If
   the clip is ever replaced, re-run tests/contrast-over-video.test.js —
   every piece of text on every public page depends on these stops.

   The enter screen carries very little text, all of it large or on its own
   solid gradient, so it can afford a light scrim and a vivid clip. */
.home-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(34, 25, 15, .58) 0%, rgba(34, 25, 15, .48) 50%, rgba(34, 25, 15, .66) 100%);
}

/* The content pages cannot. Their palette is deliberately muted —
   rgb(184,169,152) secondary text at 9-14px — and over the clip's brightest
   frames that measured as low as 1.38:1 against a 4.5:1 requirement.

   Deriving the alpha rather than guessing at it: the muted text has a
   relative luminance of ~0.409, so a 4.5:1 ratio needs a backdrop at or
   below ~0.052. Compositing #22190f over the clip's brightest pixels
   (~230/235/225) reaches that at roughly 83% opacity, so .88 is the value
   with a margin.

   This is the real cost of a site-wide video, and it is a trade: on
   /work, /bookings and /live the footage is texture and movement rather
   than a hero image. The alternative was unreadable pages. */
body[data-route="work"] .home-scrim,
body[data-route="bookings"] .home-scrim,
body[data-route="live"] .home-scrim {
  background: linear-gradient(180deg, rgba(34, 25, 15, .88) 0%, rgba(34, 25, 15, .86) 50%, rgba(34, 25, 15, .90) 100%);
}

/* Deliberately unstyled by default beyond the stacking fix below:
   home.css's scroll-snap children (.home-stage / .home-next) must not
   gain a new sizing or overflow context from an extra wrapper div. */
#site-view {
  display: block;
  /* Pairs with #site-bg's z-index: 0 above — position:relative (NOT
     transform/filter/perspective/will-change, which WOULD create a new
     containing block) keeps #site-view a no-op for its position:fixed
     descendants (.top-nav, .ipod-nano — both fixed relative to the
     viewport, per the SPA-fade comment below), while z-index: 1 puts it
     one level above #site-bg in the same (non-negative) stacking
     context. Body-level siblings outside #site-view — the glitch-click
     overlay pool, ipod-nano — use z-index 100+ (see matrix.css) and stay
     above both unaffected. */
  position: relative;
  z-index: 1;
}

/* matrix.css pages (work/bookings/live) lay their nav/main/footer out as a
   flex column via `.app-body` on <body> itself. Re-establish that one
   level down, scoped to .app-body so pages that use that class directly
   and are NOT part of the router (drop.html) are untouched. */
/* dvh after vh, same reason as .home-stage in home.css: on mobile browsers
   vh is the LARGEST viewport (chrome retracted), so a 100vh column is taller
   than the visible area and the last row hides behind the iOS toolbar. The
   vh line stays as the fallback for anything without dvh. */
body.app-body #site-view {
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

/* CSS fallback transition for browsers without the View Transitions API.
   Opacity only, never transform — #site-view must not become a new
   containing block for its position:fixed descendants (.top-nav is
   sticky, .ipod-nano is fixed). */
#site-view.spa-fade-out,
#site-view.spa-fade-in {
  transition: opacity .15s ease;
}
#site-view.spa-fade-out { opacity: 0; }
#site-view.spa-fade-in { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  #site-view.spa-fade-out,
  #site-view.spa-fade-in {
    transition: none;
  }
}

/* ── Focus indicator, site-wide ─────────────────────────────────────────
   WCAG 2.4.7 (focus visible) and 2.4.11 (focus not obscured). Several
   components across matrix.css / pages/*.css set `outline: none` on :focus
   and replace it with a border tint plus a box-shadow at alpha .1 — a
   change of roughly 1.1:1, which is not a focus indicator. Rather than
   chasing each one, this is the single authoritative ring.

   Specificity is deliberate: `.inquiry-input:focus { outline: none }` is
   (0,2,0), so a bare `:focus-visible` rule would lose. The `#site-view`
   prefix takes this to (1,2,0) and site-shell.css is the last stylesheet
   every page loads, so it wins everywhere.

   Two-tone on purpose. A bone ring is legible on the dark ground and on
   the purple/magenta/plum gradient ends; the dark halo behind it is what
   keeps it legible on the cadmium end of the LIVE and CONTACT boxes,
   where a bone-only ring would nearly vanish. Offset puts it outside the
   element, so a component that clips its own gradient with
   overflow:hidden cannot swallow it. */
#site-view a:focus-visible,
#site-view button:focus-visible,
#site-view input:focus-visible,
#site-view textarea:focus-visible,
#site-view select:focus-visible,
/* tabindex="-1" is excluded: <main> carries it only so a skip link can move
   focus there, and wrapping the entire page content in a 3px ring is not a
   useful focus indicator. */
#site-view [tabindex]:not([tabindex="-1"]):focus-visible,
#site-view summary:focus-visible {
  outline: 3px solid var(--bone);
  outline-offset: 2px;
  box-shadow: 0 0 0 6px rgba(15, 11, 6, .92);
  border-radius: 2px;
}

/* Keyboard users must be able to skip a nav they cannot see the end of.
   Only rendered when focused, so it costs nothing visually. */
#site-view .skip-link {
  position: absolute;
  left: 8px;
  top: -100px;
  z-index: 200;
  padding: 12px 18px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  background: var(--ground);
  color: var(--bone);
  font: 700 12px/1 Helvetica, Arial, sans-serif;
  letter-spacing: .14em;
  text-transform: uppercase;
  border: 1px solid var(--bone);
  transition: top .12s ease;
}

#site-view .skip-link:focus {
  top: 8px;
}

@media (prefers-reduced-motion: reduce) {
  #site-view .skip-link { transition: none; }
}

/* ── Motion pre-state ──────────────────────────────────────────────────────
   Elements the motion layer reveals start hidden, but ONLY when the inline
   head script has confirmed JS is running and the visitor has not asked for
   reduced motion. Without that gate, a JS failure or a reduced-motion user
   would be left staring at permanently invisible content. */
.js-motion [data-reveal],
.js-motion .facts,
.js-motion .placeholder,
.js-motion .press-section,
.js-motion .work-item,
.js-motion .live-track {
  opacity: 0;
}
.split-line { overflow: hidden; }
