/* ==========================================================================
   42 Studios — STORM EDITION (v2)
   - Cold realistic thunderstorm over the Newport Pell Bridge
   - Base scene: dark storm dusk — bridge/water shapes always readable
   - Rain, jagged lightning, water reflections drawn on #storm-canvas (JS)
   - Cursor carries a cold white-blue light; strikes bias toward it
   - Title revealed by the first lightning strike
   - Two-stage scroll: dock (60px) → footer (260px)  [dock currently hidden]
   ========================================================================== */

:root {
  /* Colors */
  --black: #000000;
  --ink: #050507;
  --white: #ffffff;
  --silver: #c4c4c4;
  --line: rgba(255, 255, 255, 0.18);
  --glass: rgba(8, 8, 10, 0.62);
  --glass-border: rgba(255, 255, 255, 0.10);

  /* Layout */
  --gutter: 6vw;
  --gutter-lg: 9vw;
  --header-height: 160px;
  --footer-height: 64px;
  --dock-lip: 38px;
  --dock-radius: 22px;

  /* Type — single editorial system: EB Garamond (refined, readable serif)
     paired with Inter for tight UI labels. Used consistently throughout. */
  --serif: "EB Garamond", "Libre Baskerville", "Cormorant Garamond", Georgia, serif;
  --sans: "Inter", "Manrope", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;

  /* Motion */
  --ease-luxury: cubic-bezier(0.16, 1, 0.3, 1);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* ==========================================================================
   Reset & global
   ========================================================================== */
*, *::before, *::after { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  /* Dark holding color shown instantly while the WebP background loads —
     prevents a flash of pure black/blank on slow connections. */
  background: #060a10;
  color: var(--white);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
  /* Anti-copy / anti-select — disable text highlighting site-wide */
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;             /* iOS long-press save dialog */
  -webkit-tap-highlight-color: transparent;
}

/* Anti-drag / anti-save on every image and SVG */
img, svg, picture, video {
  -webkit-user-drag: none;
  -khtml-user-drag: none;
  -moz-user-drag: none;
  -o-user-drag: none;
  user-drag: none;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: auto;                    /* keep links/buttons clickable */
}

/* Hide scrollbars — page is one cinematic viewport */
html {
  scrollbar-width: none;
  -ms-overflow-style: none;
  scroll-behavior: smooth;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
  display: none;
  width: 0;
  height: 0;
}

body {
  /* Slightly taller than viewport so a small scroll opens the dock,
     and a longer scroll opens the footer. */
  min-height: calc(100vh + 360px);
  min-height: calc(100svh + 360px);
}

a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

/* ==========================================================================
   BACKGROUND layer stack
   --------------------------------------------------------------------------
   LAYER ORDER (bottom → top):
     .bg-video       z:0  seamless-loop Newport aerial; cover-fit; cursor
                          parallax (scale 1.06 + drift) on desktop
     .dusk-veil      z:1  top-heavy cool-twilight gradient — moderate dusk
     .flash-veil     z:4  cold white screen-blend; JS drives opacity on strikes
     #storm-canvas   z:5  occasional lightning (sheet flashes + rare bolts)
     .storm-vignette z:6  cinematic edge darkening

   TUNING:
     Video grade      → .bg-video filter
     Dusk depth/cast  → .dusk-veil gradient alphas + colors
     Flash brightness → driven by JS (VEIL_PEAK in script.js)
     Parallax travel  → PARALLAX in script.js
   ========================================================================== */

.bg-video,
.dusk-veil,
.flash-veil,
.storm-vignette {
  position: fixed;
  inset: 0;
  pointer-events: none;
}

/* === VIDEO — the Higgsfield aerial, cover-fit behind everything === */
.bg-video {
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
  background: #04060b;        /* shows under the poster while it buffers */
  /* gentle cinematic grade so it sits in the brand's cool register;
     brightness pulled to dusk so the scene reads moody, lights still pop */
  filter: brightness(0.74) contrast(1.09) saturate(1.0);
}

/* === DUSK VEIL — cool twilight blue, darkest at the top (night sky),
   easing off over the lit shoreline + water below. Moderate dusk: clearly
   darker than the raw footage but the city lights and bridge still read.
   Sits on top of the video, so the parallax drift shows through it. === */
.dusk-veil {
  z-index: 1;
  background: linear-gradient(180deg,
    rgba(6, 11, 24, 0.72) 0%,
    rgba(8, 13, 27, 0.56) 24%,
    rgba(10, 15, 30, 0.36) 52%,
    rgba(11, 16, 31, 0.22) 78%,
    rgba(11, 16, 31, 0.18) 100%);
}

/* FIREWORKS MODE (body.mode-fireworks) — the procedural bursts draw on the
   canvas ABOVE the dusk veil, so we darken the VIDEO sky hard (top-heavy)
   to make the red/white/blue bursts pop, while the bridge + shoreline lights
   stay readable lower down. Storm mode is unaffected. */
.mode-fireworks .bg-video {
  filter: brightness(0.66) contrast(1.12) saturate(1.02);
}
.mode-fireworks .dusk-veil {
  background: linear-gradient(180deg,
    rgba(2, 4, 10, 0.88) 0%,
    rgba(4, 7, 15, 0.66) 26%,
    rgba(6, 10, 20, 0.34) 52%,
    rgba(8, 12, 22, 0.16) 80%,
    rgba(8, 12, 22, 0.10) 100%);
}

/* Live-weather FOG — a soft mist rising off the bay (only when it's actually
   foggy in Newport). Sits above the fireworks canvas so it gently diffuses
   the lights, but stays light enough to see everything. */
body.wx-fog::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 7;
  pointer-events: none;
  background: linear-gradient(180deg,
    rgba(150, 165, 195, 0) 38%,
    rgba(150, 165, 195, 0.08) 72%,
    rgba(160, 175, 205, 0.16) 100%);
  mix-blend-mode: screen;
}

#storm-canvas {
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  display: block;
  /* Match the video's fixed inset:0 box EXACTLY (100% of it, not 100vw/100vh).
     On iOS 100vh is taller than the visual viewport, which stretched the
     canvas backing and threw the beacon glow off the tower lights. */
  width: 100%;
  height: 100%;
}

/* === FLASH VEIL — cold white sheet, JS-driven during strikes === */
.flash-veil {
  z-index: 4;
  background: linear-gradient(180deg,
    rgba(196, 218, 255, 0.95) 0%,
    rgba(160, 192, 240, 0.80) 60%,
    rgba(120, 155, 210, 0.65) 100%);
  mix-blend-mode: screen;
  opacity: 0;                /* JS-driven per frame */
  will-change: opacity;
}

/* === VIGNETTE === */
.storm-vignette {
  z-index: 6;
  background: radial-gradient(120% 95% at 50% 50%,
    transparent 40%,
    rgba(0, 0, 0, 0.42) 80%,
    rgba(0, 0, 0, 0.80) 100%);
}

/* === MINIMALIST CURSOR PARALLAX (desktop only) ===
   The video is over-scanned (scale 1.06) so a few px of JS-driven drift
   opposite the cursor never exposes an edge. Subtle depth, no jump. */
@media (hover: hover) and (pointer: fine) {
  .bg-video {
    transform: scale(1.06) translate3d(var(--par-x, 0px), var(--par-y, 0px), 0);
    will-change: transform;
  }
}

/* Mobile — ease the vignette so the corners aren't crushed on small screens */
@media (max-width: 768px) {
  .storm-vignette {
    background: radial-gradient(125% 100% at 50% 48%,
      transparent 46%,
      rgba(0, 0, 0, 0.38) 82%,
      rgba(0, 0, 0, 0.74) 100%);
  }
}

/* ==========================================================================
   Header
   ========================================================================== */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--header-height);
  padding: 0 var(--gutter);
  padding-top: env(safe-area-inset-top);
  display: flex;
  align-items: center;
  justify-content: space-between;
  z-index: 30;
  background: transparent;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  filter: drop-shadow(0 1px 18px rgba(0, 0, 0, 0.65));
}
.brand-logo {
  display: block;
  height: 130px;
  width: auto;
  /* Preserves crisp white pixels of the logo on retina displays */
  image-rendering: -webkit-optimize-contrast;
}

.site-nav { display: flex; gap: 0; }
.site-nav a {
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.85);
  position: relative;
  padding: 6px 2px;
  text-shadow: 0 1px 18px rgba(0, 0, 0, 0.65);
  transition: color 220ms var(--ease);
}
.site-nav a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--white);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 460ms var(--ease-luxury);
}
.site-nav a:hover { color: var(--white); }
.site-nav a:hover::after { transform: scaleX(1); }

/* ==========================================================================
   Hero — title revealed by the FIRST LIGHTNING STRIKE
   ========================================================================== */
/* Fixed so the title doesn't scroll with the page — only the dock
   and footer should respond to scroll, everything else stays anchored. */
.hero {
  position: fixed;
  inset: 0;
  z-index: 10;
  padding: 0 var(--gutter-lg);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}

.hero-headline {
  margin: 0;
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(24px, 4.8vw, 64px);
  line-height: 1.1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  max-width: 100%;
  overflow: hidden;
}

.splash-text {
  display: inline-block;
  /* Override .hero { pointer-events: none } so hovering the text replays the
     shimmer. touch-action: manipulation kills the 300ms tap delay on mobile. */
  pointer-events: auto;
  touch-action: manipulation;
  cursor: default;
  /* Iridescent gradient used by the hover shimmer pass — anchored in white
     at both ends so the rest state stays pristine. */
  background-image: linear-gradient(
    105deg,
    #ffffff 0%,
    #ffffff 36%,
    #b1cdef 43%,    /* deeper cool */
    #ffd6a3 50%,    /* deeper warm */
    #c8a6ed 57%,    /* deeper violet */
    #ffffff 64%,
    #ffffff 100%
  );
  background-size: 300% 100%;
  background-repeat: no-repeat;
  background-position: 0% 50%;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
          color: transparent;

  /* Hidden until the first lightning strike reveals it */
  opacity: 0;
  letter-spacing: 0.34em;
  filter: blur(10px) brightness(3) drop-shadow(0 2px 28px rgba(0, 0, 0, 0.55));
}

/* STORM REVEAL — fired by JS in sync with the first major bolt.
   Burns in over-bright, flickers like the lightning that revealed it,
   then settles to clean white and stays. */
.hero-headline.storm-reveal .splash-text {
  animation: stormReveal 1600ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}

@keyframes stormReveal {
  0%   { opacity: 0;    letter-spacing: 0.34em; filter: blur(10px) brightness(3.2) drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 0% 50%; }
  6%   { opacity: 1;    letter-spacing: 0.22em; filter: blur(0)    brightness(2.8) drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 0% 50%; }
  14%  { opacity: 0.55; letter-spacing: 0.21em; filter: blur(0)    brightness(1.4) drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 0% 50%; }
  22%  { opacity: 1;    letter-spacing: 0.20em; filter: blur(0)    brightness(2.2) drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 0% 50%; }
  42%  { opacity: 1;    letter-spacing: 0.19em; filter: blur(0)    brightness(1.3) drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 30% 50%; }
  100% { opacity: 1;    letter-spacing: 0.18em; filter: blur(0)    brightness(1)   drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 100% 50%; }
}

/* Hover replay — chromatic shimmer pass only. Text stays fully visible
   throughout (opacity locked at 1). No fade out, no disappearing. */
.hero-headline.is-flashing .splash-text {
  animation: splashFlash 1400ms cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}

@keyframes splashFlash {
  0%   { opacity: 1; letter-spacing: 0.18em; filter: brightness(1)    drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 0% 50%; }
  50%  { opacity: 1; letter-spacing: 0.18em; filter: brightness(1.25) drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 50% 50%; }
  100% { opacity: 1; letter-spacing: 0.18em; filter: brightness(1)    drop-shadow(0 2px 28px rgba(0,0,0,0.55)); background-position: 100% 50%; }
}

/* ==========================================================================
   Live conditions chip — bottom-right, fed by the real Newport fetch.
   Hidden until data arrives; lifts above the footer when it slides up.
   ========================================================================== */
.weather-chip {
  position: fixed;
  right: 22px;
  bottom: calc(20px + env(safe-area-inset-bottom));
  z-index: 27;
  font-family: var(--sans);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.34em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.40);
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.6);
  pointer-events: none;
  opacity: 0;
  transition:
    opacity 1200ms var(--ease),
    transform 620ms var(--ease-luxury),
    color 220ms var(--ease);
}
.weather-chip.is-live { opacity: 1; }
body.footer-active .weather-chip {
  transform: translateY(calc(-1 * var(--footer-height)));
}

/* MOBILE/touch: chip stays bottom-right; the open dock covers the bottom,
   so fade it out while the dock is up to avoid colliding with the tiles. */
@media (max-width: 560px) {
  .weather-chip {
    right: 14px;
    font-size: 8px;
    letter-spacing: 0.26em;
  }
}
body.dock-active .weather-chip { opacity: 0; pointer-events: none; }

/* DESKTOP: move the wind chip directly under the Contact link (top-right),
   right-aligned to the same gutter. It no longer sits at the bottom, so it
   never collides with the open dock — keep it visible when the dock is up. */
@media (hover: hover) and (pointer: fine) {
  .weather-chip {
    top: calc(env(safe-area-inset-top) + 100px);
    right: var(--gutter);
    bottom: auto;
    text-align: right;
  }
  body.footer-active .weather-chip { transform: none; }   /* stays put up top */
  body.dock-active .weather-chip { opacity: 1; }
}

/* ==========================================================================
   PRIVATE PORTFOLIO — passcode gate (code 4246, session-only)
   Centered over the live scene. Desktop: mono digit cells (type to fill).
   Touch: iOS-26-style dots + frosted keypad. On success the dock rises and
   the lock fades. JS adds .lock--desktop / .lock--touch to route the UI.
   ========================================================================== */
.lock {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--header-height) 6vw calc(var(--footer-height) + 34px);
  text-align: center;
  pointer-events: none;                 /* only .lock-inner captures input */
  transition: opacity 760ms var(--ease-luxury), transform 760ms var(--ease-luxury);
}
.lock-inner {
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.lock-title {
  margin: 0;
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(20px, 3.4vw, 34px);
  letter-spacing: 0.42em;
  text-indent: 0.42em;                  /* optical-center the tracked caps */
  text-transform: uppercase;
  color: #fff;
  text-shadow: 0 2px 30px rgba(0, 0, 0, 0.6);
}
.lock-sub {
  margin: 16px 0 28px;
  font-family: var(--sans);
  font-weight: 400;
  font-size: 10.5px;
  letter-spacing: 0.40em;
  text-indent: 0.40em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}

/* shared code row + shake on wrong code (iOS-style) */
.code { display: flex; align-items: center; justify-content: center; }
.code.shake { animation: codeShake 460ms cubic-bezier(0.36, 0.07, 0.19, 0.97); }
@keyframes codeShake {
  10%, 90% { transform: translateX(-2px); }
  20%, 80% { transform: translateX(4px); }
  30%, 50%, 70% { transform: translateX(-8px); }
  40%, 60% { transform: translateX(8px); }
}

/* Desktop — monospace digit cells */
.code--cells { gap: 16px; }
.code-cell {
  width: clamp(42px, 4vw, 52px);
  height: clamp(54px, 5vw, 66px);
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.04);
  display: grid;
  place-items: center;
  font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  font-size: clamp(24px, 2.6vw, 30px);
  color: #fff;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
  transition: border-color 240ms var(--ease), background 240ms var(--ease), box-shadow 240ms var(--ease);
}
.code-cell.filled { border-color: rgba(255, 255, 255, 0.45); background: rgba(255, 255, 255, 0.08); }
.code-cell.active {
  border-color: rgba(255, 255, 255, 0.7);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.code-cell.active:not(.filled)::after {
  content: "";
  width: 1.5px;
  height: 46%;
  background: #fff;
  animation: caretBlink 1.1s steps(1) infinite;
}
@keyframes caretBlink { 0%, 50% { opacity: 0.85; } 50.01%, 100% { opacity: 0; } }

/* Touch — iOS dots */
.code--dots { gap: 22px; }
.code-dot {
  width: 13px; height: 13px;
  border-radius: 50%;
  border: 1.5px solid rgba(255, 255, 255, 0.65);
  transition: background 150ms var(--ease), border-color 150ms var(--ease);
}
.code-dot.on { background: #fff; border-color: #fff; }

/* Touch — iOS-26 lockscreen keypad (frosted "liquid glass" keys) */
.keypad {
  margin-top: 30px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(14px, 4vw, 22px) clamp(18px, 6vw, 28px);
  width: min(300px, 80vw);
}
/* iOS lockscreen key — faithful to the real material (verified against
   Apple's Liquid Glass language): a FLAT, ultra-thin frosted circle.
   ~13% white over heavy backdrop blur, no border, no dome gradient, no drop
   shadow. The "glass" reads through (1) the blur of the scene behind it and
   (2) a whisper of edge lensing: a hairline inset ring that's brighter on
   the top edge, where glass catches light. Press = the fill brightens
   instantly (iOS behavior), nothing scales or bounces. */
.key {
  position: relative;
  width: clamp(58px, 19vw, 76px);
  height: clamp(58px, 19vw, 76px);
  margin: 0 auto;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.13);
  backdrop-filter: blur(24px) saturate(160%);
  -webkit-backdrop-filter: blur(24px) saturate(160%);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  box-shadow:
    inset 0 0 0 0.5px rgba(255, 255, 255, 0.18),   /* hairline edge */
    inset 0 1px 0 rgba(255, 255, 255, 0.22),       /* top edge lensing */
    inset 0 -0.5px 0 rgba(255, 255, 255, 0.05);    /* faint bottom catch */
  transition: background 90ms linear;
}
.key:active { background: rgba(255, 255, 255, 0.34); }
.key--blank { background: none; box-shadow: none; backdrop-filter: none; -webkit-backdrop-filter: none; pointer-events: none; }
.key--del {
  background: none; box-shadow: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  font-size: 22px; color: rgba(255, 255, 255, 0.85);
}
.key--del:active { background: none; }
/* iOS digits are SF (the system face), light weight — not monospace */
.key-d {
  font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Helvetica Neue", Arial, sans-serif;
  font-size: clamp(27px, 8.4vw, 34px);
  font-weight: 300;
  line-height: 1;
}

/* iOS Taptic trigger — visually hidden but clickable in script */
.haptic-switch, .haptic-switch-label {
  position: fixed;
  left: -9999px; top: 0;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* success pulse before reveal */
.code.ok .code-cell { border-color: #fff; background: rgba(255, 255, 255, 0.12); }
.code.ok .code-dot { background: #fff; border-color: #fff; }

/* UI routing — JS sets .lock--desktop or .lock--touch */
.code--dots, .keypad { display: none; }
.lock--touch  .code--cells { display: none; }
.lock--touch  .code--dots  { display: flex; }
.lock--touch  .keypad      { display: grid; }
.lock--desktop .code--cells { display: flex; }

/* UNLOCKED — fade the lock out; the dock takes over */
body.unlocked .lock {
  opacity: 0;
  transform: translateY(-10px) scale(0.985);
  pointer-events: none;
}

/* ==========================================================================
   Portfolio Dock — hidden until the access code unlocks it
   ========================================================================== */
/* Stay fully off-screen (no peeking lip) while locked */
body:not(.unlocked) .dock-wrap { transform: translateY(120%) !important; pointer-events: none; }

.dock-wrap {
  position: fixed;
  left: 0; right: 0;
  bottom: 0;
  z-index: 25;
  padding: 0 5vw;
  pointer-events: none;
  transform: translateY(calc(100% - var(--dock-lip)));
  transition: transform 720ms var(--ease-luxury);
  will-change: transform;
}

/* STAGE 1 — Dock active, footer hidden: dock sits flush at the bottom */
body.dock-active .dock-wrap   { transform: translateY(0); }

/* STAGE 2 — Footer also active: dock lifts to make room above the footer */
body.footer-active .dock-wrap { transform: translateY(calc(-1 * var(--footer-height))); }

.dock {
  pointer-events: auto;
  position: relative;
  width: 100%;
  background: var(--glass);
  backdrop-filter: blur(22px) saturate(120%);
  -webkit-backdrop-filter: blur(22px) saturate(120%);
  border: 1px solid var(--glass-border);
  border-bottom: none;
  border-radius: var(--dock-radius) var(--dock-radius) 0 0;
  box-shadow:
    0 -30px 80px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.05);
  overflow: hidden;
}

.dock-handle {
  height: var(--dock-lip);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
  cursor: default;       /* dock is pinned open after unlock — not a control */
  user-select: none;
}
/* "PRIVATE PORTFOLIO" dock label — Succession iridescent glare:
   anchored white at both ends, a chromatic band sweeps across on a slow
   recurring loop. JS adds .burn for the one-time burn-in glint on reveal. */
.dock-handle-label {
  font-family: var(--sans);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.46em;
  text-transform: uppercase;
  text-indent: 0.46em;                  /* optical-center tracked caps */
  background-image: linear-gradient(
    105deg,
    #ffffff 0%, #ffffff 38%,
    #b1cdef 44%,      /* cool */
    #ffd6a3 50%,      /* warm */
    #c8a6ed 56%,      /* violet */
    #ffffff 62%, #ffffff 100%);
  background-size: 300% 100%;
  background-repeat: no-repeat;
  background-position: 0% 50%;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
          color: transparent;
  animation: labelGlint 7s ease-in-out infinite;
}
/* slow recurring sweep: glides the chromatic band across once, then rests
   white (both ends are white, so the loop restart is seamless) */
@keyframes labelGlint {
  0%   { background-position: 0% 50%; }
  16%  { background-position: 100% 50%; }
  100% { background-position: 100% 50%; }
}
/* one-time burn-in glint the moment the dock reveals */
.dock-handle-label.burn { animation: labelBurn 1500ms cubic-bezier(0.22, 0.61, 0.36, 1), labelGlint 7s ease-in-out 1500ms infinite; }
@keyframes labelBurn {
  0%   { opacity: 0.15; letter-spacing: 0.62em; filter: brightness(2.6) drop-shadow(0 0 16px rgba(180, 210, 255, 0.6)); background-position: 0% 50%; }
  10%  { opacity: 1;    filter: brightness(2.4) drop-shadow(0 0 14px rgba(180, 210, 255, 0.55)); }
  45%  { background-position: 100% 50%; }
  100% { opacity: 1;    letter-spacing: 0.46em; filter: brightness(1) drop-shadow(0 0 0 rgba(0, 0, 0, 0)); background-position: 100% 50%; }
}
/* (no hover replay — swapping animations mid-loop made the label flicker;
   the slow ambient glint is the only sweep) */
.dock-handle-rule {
  display: inline-block;
  width: 36px;
  height: 1px;
  background: rgba(255, 255, 255, 0.30);
  transition: width 460ms var(--ease-luxury), background 260ms var(--ease);
}
body.dock-active .dock-handle-rule {
  width: 64px;
  background: rgba(255, 255, 255, 0.55);
}

.dock-body { padding: 18px 22px 22px; }

.dock-track {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 96px;
  gap: 14px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  scroll-behavior: smooth;
  -webkit-overflow-scrolling: touch;
}
.dock-track::-webkit-scrollbar { display: none; }

.tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 4px;
  cursor: default;       /* tiles are showcase, not links — no false pointer */
  transition: transform 460ms var(--ease-luxury);
}
.tile-icon {
  position: relative;
  width: 72px;
  height: 72px;
  border-radius: 18px;
  background: linear-gradient(160deg, #1c1c1f 0%, #0a0a0c 100%);
  border: 1px solid rgba(255, 255, 255, 0.12);
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255, 255, 255, 0.92);
  overflow: hidden;
  box-shadow:
    0 14px 30px rgba(0, 0, 0, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.05);
  transition:
    transform 460ms var(--ease-luxury),
    box-shadow 460ms var(--ease),
    border-color 460ms var(--ease);
}
.tile-icon::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(180deg, rgba(255,255,255,0.06) 0%, rgba(255,255,255,0) 45%);
  pointer-events: none;
  z-index: 2;
}
/* App icon image fills the tile cleanly */
.tile-icon img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
  display: block;
  position: relative;
  z-index: 1;
}
/* Coming Soon — minimal monochrome loader mark */
.tile-soon .tile-icon {
  background: linear-gradient(160deg, #141416 0%, #07070a 100%);
}
.tile-icon .soon-mark {
  width: 30px;
  height: 30px;
  fill: none;
  position: relative;
  z-index: 1;
  animation: soonSpin 4.8s linear infinite;
}
@keyframes soonSpin {
  to { transform: rotate(360deg); }
}

/* Two-line label: APP TITLE on top, YEAR underneath. App names keep their
   real casing (RentPal, ToneDeaf, u up?) — no uppercase transform. */
.tile-label {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  font-family: var(--sans);
  white-space: nowrap;
  transition: color 220ms var(--ease);
}
.tile-title {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.06em;
  color: rgba(255, 255, 255, 0.85);
}
.tile-year {
  font-size: 8.5px;
  font-weight: 400;
  letter-spacing: 0.28em;
  color: rgba(255, 255, 255, 0.45);
}

.tile:hover .tile-icon {
  border-color: rgba(255, 255, 255, 0.32);
  box-shadow:
    0 22px 44px rgba(0, 0, 0, 0.6),
    0 0 0 1px rgba(255, 255, 255, 0.04),
    0 0 26px rgba(255, 255, 255, 0.08),
    inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.tile:hover .tile-title { color: var(--white); }
.tile:hover .tile-year  { color: rgba(255, 255, 255, 0.7); }

/* ==========================================================================
   Footer — hidden until STAGE 2
   ========================================================================== */
.site-footer {
  position: fixed;
  left: 0; right: 0;
  bottom: 0;
  height: calc(var(--footer-height) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--white);
  color: var(--black);
  z-index: 28;
  /* GPU-accelerated slide — promotes the layer so transform animates without
     repaints, which keeps the mobile swipe-up smooth. */
  will-change: transform;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  transform: translateY(110%);
  transition: transform 620ms var(--ease-luxury) 200ms;
}
body.footer-active .site-footer { transform: translateY(0); }

.footer-inner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: center;
  gap: 18px;
  font-family: var(--sans);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: #111;
  padding: 0 5vw;
  text-align: center;
}
.footer-inner a { color: #111; transition: color 220ms var(--ease); }
.footer-inner a:hover { color: #555; }
.footer-inner .sep { color: rgba(0, 0, 0, 0.35); }
.footer-copy { white-space: nowrap; }
.footer-rights { white-space: nowrap; }

/* ==========================================================================
   TOUCH DEVICES (phones + tablets) — locked single screen
   - No scrolling at all: the page is exactly one viewport tall
   - Footer is ALWAYS up (no swipe needed); weather chip rides above it
   - Legal pages are excluded — they keep their natural document scroll
   ========================================================================== */
@media (hover: none), (pointer: coarse) {
  html { overflow: hidden; height: 100%; overscroll-behavior: none; }
  body:not(.page-legal) {
    min-height: 100vh;  min-height: 100svh;
    height: 100vh;      height: 100svh;
    overflow: hidden;
    overscroll-behavior: none;
    touch-action: manipulation;     /* taps stay snappy, no double-tap zoom */
  }
  body:not(.page-legal) .site-footer {
    transform: translateY(0);       /* pinned up, no slide */
    transition: none;
  }
  body:not(.page-legal) .weather-chip {
    transform: translateY(calc(-1 * var(--footer-height)));
  }
}

/* (Compact phone-footer SIZING lives in the authoritative block at the very
   end of this file so it wins the cascade — see "PHONE FOOTER (authoritative)".
   This block only sets the touch-specific footer height.) */
@media (pointer: coarse) and (max-width: 600px), (hover: none) and (max-width: 600px) {
  :root { --footer-height: 44px; }
}

/* ==========================================================================
   Responsive
   ========================================================================== */
@media (max-width: 900px) {
  :root {
    --gutter: 5vw;
    --gutter-lg: 6vw;
    --footer-height: 56px;
    --dock-lip: 32px;
    --header-height: 108px;
  }
  .brand-logo { height: 84px; }
  .site-nav a { font-size: 10px; letter-spacing: 0.36em; }

  .hero-headline { font-size: clamp(20px, 5.6vw, 42px); letter-spacing: 0.16em; }

  .dock-track { grid-auto-columns: 78px; gap: 10px; }
  .tile-icon { width: 58px; height: 58px; border-radius: 14px; }
  .tile-icon svg { width: 22px; height: 22px; }
  .tile-icon .soon-mark { width: 22px; height: 22px; }
  .tile-title { font-size: 8.5px; letter-spacing: 0.22em; }
  .tile-year  { font-size: 7.5px; letter-spacing: 0.20em; }

  .footer-inner { font-size: 9px; gap: 12px; letter-spacing: 0.26em; }
}

@media (max-width: 560px) {
  :root { --header-height: 92px; }
  .site-header { padding: 0 var(--gutter); }
  .brand { gap: 10px; }
  .brand-logo { height: 64px; }

  .hero { padding: 0 var(--gutter); }
  .hero-headline { font-size: clamp(18px, 5.2vw, 30px); letter-spacing: 0.14em; }

  /* Compact footer on phones — single line, short labels:
     "© 2026 42 Studios LLC. | Privacy | Terms | Contact"
     (.footer-rights + .f-long are hidden by the ≤600px touch block) */
  .footer-rights, .f-long { display: none; }
}

@media (max-width: 380px) {
  .hero-headline { font-size: 16px; letter-spacing: 0.12em; }
  .brand-logo { height: 48px; }
}

/* ==========================================================================
   PHONE FOOTER (authoritative) — LAST footer rule in the file, so it wins
   the cascade over the 900px and 560px blocks above (equal specificity, later
   source = winner). Width-based (no pointer test) so it applies on every real
   phone AND is verifiable in preview. Type + spacing + gaps all scale with
   screen width, so "© 2026 42 STUDIOS LLC. | PRIVACY | TERMS | CONTACT" fits
   one line at any phone size and can't clip on either edge.
   ========================================================================== */
@media (max-width: 600px) {
  .footer-rights, .f-long { display: none; }
  .footer-inner {
    width: 100%;
    flex-wrap: nowrap;
    white-space: nowrap;
    justify-content: center;
    gap: clamp(5px, 1.6vw, 9px);
    font-size: clamp(7px, 2.15vw, 9px);
    letter-spacing: 0.06em;
    padding: 0 2vw;
  }
}

/* ==========================================================================
   MOBILE CHART — ranked glass-capsule list (the Divino "Rising through the
   App Store" piece, restyled in 42 monochrome). TOUCH DEVICES ONLY, shown
   once unlocked — it replaces the horizontal dock there. JS adds
   body.touch-ui on coarse-pointer devices; desktop keeps the dock.
   ========================================================================== */
.chart-wrap {
  position: fixed;
  inset: 0;
  z-index: 18;                          /* under header(30) + lock(20) */
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
  overflow: hidden;
  padding:
    calc(var(--header-height) + env(safe-area-inset-top))
    5vw
    calc(var(--footer-height) + 14px + env(safe-area-inset-bottom));
}
body.touch-ui.unlocked .chart-wrap { display: flex; }
body.touch-ui .dock-wrap { display: none !important; }   /* chart replaces dock */

.chart-head {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  opacity: 0;
  transition: opacity 700ms var(--ease-luxury) 150ms;
}
body.unlocked .chart-head { opacity: 1; }
.chart-rule {
  display: inline-block;
  width: 42px;
  height: 1px;
  background: rgba(255, 255, 255, 0.30);
}
/* same Succession glare as the dock label */
.chart-label {
  font-family: var(--sans);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: 0.46em;
  text-indent: 0.46em;
  text-transform: uppercase;
  background-image: linear-gradient(
    105deg,
    #ffffff 0%, #ffffff 38%,
    #b1cdef 44%, #ffd6a3 50%, #c8a6ed 56%,
    #ffffff 62%, #ffffff 100%);
  background-size: 300% 100%;
  background-repeat: no-repeat;
  background-position: 0% 50%;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
          color: transparent;
  animation: labelGlint 7s ease-in-out infinite;
}

.chart {
  list-style: none;
  margin: 0;
  padding: 2px;
  display: flex;
  flex-direction: column;
  gap: 9px;
  /* CRITICAL: min-height:0 lets this flex child SHRINK to the available
     space so overflow-y actually engages — without it the list grows past
     the screen and slides under the footer with no way to reach the tail. */
  min-height: 0;
  overflow-y: auto;
  touch-action: pan-y;                  /* override the page-level lock */
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.chart::-webkit-scrollbar { display: none; }

/* staggered drop-in once unlocked (--d set per row by JS, top to bottom) */
.chart-row {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 640ms var(--ease-luxury), transform 640ms var(--ease-luxury);
}
body.unlocked .chart-row {
  opacity: 1;
  transform: none;
  transition-delay: calc(var(--d, 0) * 110ms + 200ms);
}

/* the glass capsule: rank · icon · name/meta · year pill */
.row-link {
  position: relative;
  display: grid;
  grid-template-columns: auto auto 1fr auto;
  align-items: center;
  gap: 11px;
  padding: 8px 13px 8px 10px;
  border-radius: 18px;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(255, 255, 255, 0.025));
  border: 1px solid rgba(255, 255, 255, 0.13);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.16),
    inset 0 -1px 0 rgba(0, 0, 0, 0.45),
    0 14px 32px rgba(0, 0, 0, 0.42);
  overflow: hidden;
}
/* the diagonal sheen — the liquid part */
.row-link::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(115deg, rgba(255, 255, 255, 0.09), transparent 42%);
  pointer-events: none;
}
@supports not (backdrop-filter: blur(1px)) {
  .row-link { background: linear-gradient(180deg, rgba(24, 26, 32, 0.92), rgba(11, 13, 18, 0.92)); }
}

.row-icon {
  width: 42px; height: 42px;
  border-radius: 11px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.14);
  display: grid;
  place-items: center;
  background: linear-gradient(160deg, #1c1c1f 0%, #0a0a0c 100%);
}
.row-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }
.row-soon .row-icon { border-style: dashed; background: none; }
.row-soon .row-icon .soon-mark { width: 22px; height: 22px; }
.row-soon .row-link { opacity: 0.85; }

.row-rank {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 14px;
  color: rgba(255, 255, 255, 0.55);
  min-width: 16px;
  text-align: center;
}
.row-text { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.row-name {
  font-family: var(--sans);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.05em;         /* real casing (RentPal, u up?) — tight */
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.row-meta {
  font-family: var(--sans);
  font-size: 8.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.45);
}
.row-pill {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px;
  letter-spacing: 0.06em;
  padding: 6px 12px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  background: rgba(255, 255, 255, 0.06);
  color: rgba(255, 255, 255, 0.82);
  white-space: nowrap;
}

/* ==========================================================================
   Legal pages — Privacy Policy / Terms of Service
   These rules only apply when <body class="page-legal"> is set.
   ========================================================================== */
body.page-legal {
  background: #050507;
  min-height: 100vh;
  min-height: 100svh;
  cursor: auto;
}
body.page-legal .bg-video,
body.page-legal .dusk-veil,
body.page-legal .flash-veil,
body.page-legal #storm-canvas,
body.page-legal .storm-vignette { display: none; }

/* Header on legal pages: transparent — page background is solid dark already */
body.page-legal .site-header {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Footer on legal pages sits at the natural document end, not pinned */
body.page-legal .site-footer {
  position: relative;
  bottom: auto;
  transform: none;
  margin-top: 80px;
}

.legal {
  position: relative;
  z-index: 5;
  max-width: 780px;
  margin: 0 auto;
  padding: calc(var(--header-height) + 80px) var(--gutter-lg) 80px;
  color: rgba(255, 255, 255, 0.86);
  font-family: var(--serif);
}

.legal-eyebrow {
  display: inline-block;
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.55);
  margin-bottom: 22px;
}

.legal-title {
  margin: 0 0 14px;
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(36px, 5.6vw, 64px);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--white);
  line-height: 1.05;
}

.legal-effective {
  margin: 0 0 56px;
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.5);
}

.legal section { margin-bottom: 38px; }

.legal h2 {
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(18px, 2vw, 22px);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--white);
  margin: 0 0 16px;
  line-height: 1.3;
}

.legal p,
.legal li {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 17px;
  line-height: 1.75;
  color: rgba(255, 255, 255, 0.82);
  margin: 0 0 14px;
}

.legal p:last-child,
.legal li:last-child { margin-bottom: 0; }

.legal strong {
  color: var(--white);
  font-weight: 600;
}

.legal a {
  color: var(--white);
  border-bottom: 1px solid rgba(255, 255, 255, 0.4);
  transition: border-color 220ms var(--ease);
}
.legal a:hover { border-bottom-color: var(--white); }

.legal ul {
  list-style: none;
  padding: 0;
  margin: 0 0 14px;
}
.legal ul li {
  position: relative;
  padding-left: 22px;
  margin-bottom: 8px;
}
.legal ul li::before {
  content: "—";
  position: absolute;
  left: 0;
  top: 0;
  color: rgba(255, 255, 255, 0.35);
}

.legal-block {
  margin-top: 56px;
  padding-top: 32px;
  border-top: 1px solid rgba(255, 255, 255, 0.10);
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.7);
  line-height: 2;
}
.legal-block strong {
  display: block;
  font-weight: 600;
  letter-spacing: 0.32em;
}

@media (max-width: 700px) {
  .legal {
    padding: calc(var(--header-height) + 56px) var(--gutter) 56px;
  }
  .legal h2 { font-size: 16px; letter-spacing: 0.14em; }
  .legal p, .legal li { font-size: 16px; }
}

/* ==========================================================================
   Reduced motion — static stormy scene, no flashes, no rain, no audio
   (script.js also skips the entire storm engine when this matches)
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .splash-text {
    opacity: 1;
    letter-spacing: 0.18em;
    filter: brightness(1) drop-shadow(0 2px 28px rgba(0, 0, 0, 0.55));
  }
}
