/* ==========================================================================
   Trigger — marketing landing page
   Dark base + glow accents pulled 1:1 from the real product's map_view.py
   ICON_CATEGORIES / phase system (see comments on each --data-* variable).
   ========================================================================== */

:root {
  /* base surfaces */
  --bg-void: #05070a;
  --bg-deep: #090c11;
  --bg-panel: #10141b;
  --border-soft: rgba(255, 255, 255, 0.09);
  --border-softer: rgba(255, 255, 255, 0.05);

  /* type */
  --text-primary: #f2f4f7;
  --text-secondary: #9aa4b6;
  --text-tertiary: #647087;

  /* brand chrome — decorative only, never used to represent real trigger data.
     Radar green is now primary (the map/radar narrative, used everywhere:
     nav, focus states, eyebrows, the radar sweep itself, decorative glows).
     Coral is demoted to a rare, deliberate highlight -- ONLY the main
     "Request access" CTA (.cta-primary) and its nav-pill twin (.cta-pill)
     still use it, so the one thing we most want clicked stays visually
     distinct from the green atmosphere around it. */
  --brand-green: #33ff8c;
  --brand-green-glow: rgba(51, 255, 140, 0.45);
  --brand-coral: #ff5d52;
  --brand-glow: rgba(255, 93, 82, 0.45);

  /* real product data colors — verbatim from map_view.py ICON_CATEGORIES + TIER_PHASE */
  --data-power: #f59e0b;    /* bolt: power outage */
  --data-flood: #2563eb;    /* droplet: flood / flash-flood — also the "before" phase color */
  --data-rain: #0891b2;     /* heavy rain */
  --data-freeze: #0ea5e9;   /* snowflake: freeze */
  --data-urgent: #dc2626;   /* flame: heat wave — also the "during" phase color */
  --data-storm: #7c3aed;    /* hail / tornado / winter storm / etc. */
  --data-after: #6b7280;    /* "after" phase color */
  --data-severity: #facc15; /* top-severity gold ring */

  --nav-height: 64px;
  --edge-pad: clamp(24px, 5vw, 72px);

  /* shared easing — used for every hover/focus/press/reveal transition */
  --ease-standard: cubic-bezier(0.4, 0, 0.2, 1);
  --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg-void);
  color: var(--text-primary);
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

h1, h2, h3, p, ul, form { margin: 0; }
a { color: inherit; text-decoration: none; }

button {
  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: none;
  font: inherit;
  color: inherit;
  cursor: pointer;
}

button, input, select, textarea { font-family: inherit; }

a:focus-visible,
button:focus-visible {
  outline: 2px solid var(--brand-green);
  outline-offset: 3px;
  border-radius: 4px;
}

.eyebrow {
  display: inline-block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--brand-green);
  margin-bottom: 18px;
}

/* ---------- scroll-snap container ---------- */

.scroll-container {
  height: 100vh;
  height: 100dvh; /* 100vh assumes mobile browser chrome is hidden; dvh tracks the real visible viewport */
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scrollbar-width: none; /* Firefox */
}

/* Safety net: even with the dvh fix and the mobile content audit below,
   "mandatory" snap can still yank a small in-section scroll straight to
   the next section if anything is ever a few px taller than the real
   visible viewport. "proximity" only snaps near a snap point and lets
   scrolling within an overflowing section work normally otherwise. */
@media (max-width: 780px) {
  .scroll-container { scroll-snap-type: y proximity; }
}

.scroll-container::-webkit-scrollbar {
  display: none; /* Chromium/WebKit — dot nav is the scroll indicator */
}

.panel {
  min-height: 100vh;
  min-height: 100dvh;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  overflow: hidden;
}

/* ---------- depth layers: background photo, scrim, floating orbs ---------- */
/* Background/orb parallax offsets are written to CSS vars by script.js on
   scroll/mousemove; oversizing the bg 16% on each edge means the parallax
   translate can never expose an edge. */

.section-bg {
  position: absolute;
  inset: -8%;
  width: 116%;
  height: 116%;
  object-fit: cover;
  z-index: 0;
  transform: translateY(var(--parallax-y, 0px));
  will-change: transform;
}

.section-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
}

/* ---------- top nav ---------- */

.topnav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: var(--nav-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 var(--edge-pad);
  z-index: 100;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background 0.5s var(--ease-standard), border-color 0.5s var(--ease-standard),
    backdrop-filter 0.5s var(--ease-standard);
}

/* Toggled by script.js (see updateNavChrome) once scrolled past the hero.
   Transparent over the hero -- its own imagery/vignette already gives short
   nav text plenty of contrast -- glass only once fixed nav chrome needs to
   separate itself from arbitrary section content sliding underneath it. */
.topnav.is-scrolled {
  background: rgba(8, 10, 14, 0.58);
  backdrop-filter: blur(16px) saturate(140%);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  border-bottom-color: var(--border-softer);
}

.wordmark {
  display: flex;
  align-items: center;
  gap: 9px;
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.09em;
  transition: opacity 0.25s var(--ease-standard);
}

.wordmark:hover { opacity: 0.72; }

.brand-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--brand-green);
  box-shadow: 0 0 7px 1px var(--brand-green-glow);
  animation: brand-pulse 2.4s ease-in-out infinite;
}

@keyframes brand-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.55; transform: scale(0.8); }
}

/* Deliberately quiet at rest (neutral ghost border, no coral) rather than
   the old permanently coral-tinted pill -- sitting directly over the hero's
   own coral radar glow on a transparent nav, a second coral shape read as
   competing rather than confirming. Coral is reserved for the hover state,
   consistent with how .cta-primary elsewhere only glows on interaction. */
.cta-pill {
  display: inline-flex;
  align-items: center;
  font-size: 12.5px;
  font-weight: 600;
  letter-spacing: 0.01em;
  padding: 8px 16px;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.03);
  transition: background 0.3s var(--ease-standard), border-color 0.3s var(--ease-standard),
    transform 0.3s var(--ease-out-expo), box-shadow 0.3s var(--ease-standard);
}

.cta-pill:hover {
  border-color: rgba(255, 93, 82, 0.6);
  background: rgba(255, 93, 82, 0.1);
  box-shadow: 0 0 20px -4px var(--brand-glow);
  transform: translateY(-1px);
}

.cta-pill:active { transform: translateY(0) scale(0.96); }

/* ---------- dot nav ---------- */

.dotnav {
  position: fixed;
  right: 28px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

/* Brighter ring + a dark drop-shadow ground: these sit fixed over
   whatever section is scrolled past, including the hero's now-visible
   (and locally bright) photo, where a plain 0.35-alpha white ring on
   transparent could disappear entirely against a light patch. */
.dotnav .dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.55);
  background: rgba(5, 7, 10, 0.35);
  cursor: pointer;
  box-shadow: 0 1px 4px rgba(5, 7, 10, 0.6);
  transition: transform 0.35s var(--ease-out-expo), background 0.35s var(--ease-standard),
    border-color 0.35s var(--ease-standard), box-shadow 0.35s var(--ease-standard);
}

.dotnav .dot:hover { border-color: #fff; }

.dotnav .dot.active {
  background: var(--brand-green);
  border-color: var(--brand-green);
  box-shadow: 0 0 6px 1px var(--brand-green-glow);
  transform: scale(1.3);
}

@media (max-width: 780px) {
  .dotnav { display: none; }
}

/* ---------- mockup chrome (browser/device frames) ----------
   Shared between the demo section's video frame and the dashboard
   section's desktop screenshot -- a real browser-chrome titlebar
   (dots + a fake URL pill) instead of presenting either as a raw,
   edge-cropped screenshot. */

.browser-frame-bar {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 14px;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.035), transparent);
  border-bottom: 1px solid var(--border-softer);
}

.browser-frame-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.16);
}

.browser-frame-url {
  margin-left: 8px;
  font-size: 10.5px;
  color: var(--text-tertiary);
  letter-spacing: 0.02em;
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid var(--border-softer);
  border-radius: 999px;
  padding: 3px 12px;
}

/* Dashboard's desktop screenshot: a full browser window, not just a
   titlebar strip -- rounded card, hairline border, and a glow using the
   same brand-green-glow accent every other elevated surface on the page
   uses now (radar green is primary; coral is reserved for the CTA). */
.browser-frame {
  display: flex;
  flex-direction: column;
  max-width: 100%;
  max-height: 100%;
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg-panel);
  border: 1px solid var(--border-soft);
  box-shadow: 0 24px 50px -16px rgba(0, 0, 0, 0.6), 0 0 70px -24px var(--brand-green-glow);
}

.browser-frame img {
  display: block;
  width: 100%;
  flex: 1;
  min-height: 0;
  object-fit: contain;
  background: #000;
}

/* Dashboard's mobile screenshot: a bezeled device-style card (thicker
   rounded border, no titlebar) rather than a phone notch/home-indicator --
   the real screenshot's own proportions (~1.2:1, wider than tall, a
   cropped review panel, not a true full-screen phone capture) never look
   right forced into an actual notched-phone shape, so this reads as
   "device" through weight/rounding instead of literal chrome that would
   misrepresent what the image actually is. */
.device-frame {
  border-radius: 22px;
  overflow: hidden;
  border: 3px solid rgba(255, 255, 255, 0.12);
  background: #000;
  box-shadow: 0 20px 50px -16px rgba(0, 0, 0, 0.6), 0 0 60px -22px var(--brand-green-glow);
}

.device-frame img {
  display: block;
  width: 100%;
  height: auto;
}

/* ---------- shared layout helpers ---------- */

.section-head {
  position: relative;
  z-index: 2;
  text-align: center;
  max-width: 720px;
  margin: 0 auto 56px;
  padding: 0 var(--edge-pad);
}

.section-head h2 { text-align: center; }

h2 {
  font-size: clamp(28px, 4vw, 44px);
  font-weight: 300;
  line-height: 1.2;
  letter-spacing: 0.01em;
}

/* ---------- grain overlay ---------- */

/* Finer tile (128px, up from 180px) reads less as an obviously-repeating
   pattern at normal viewing distance; a touch more opacity than before
   (0.045, up from 0.035) so it still reads as a texture now that the
   per-section scrims above carry more color -- grain is what keeps
   those color areas feeling like one consistent material instead of
   flat gradient shapes. */
.grain-overlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  pointer-events: none;
  opacity: 0.045;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 128px 128px;
}

/* ---------- scroll-triggered reveal ---------- */

.reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity 0.9s var(--ease-out-expo), transform 0.9s var(--ease-out-expo);
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ==========================================================================
   1. HERO
   ========================================================================== */

.hero {
  background: var(--bg-void);
}

/* Lift the hero photo itself before anything darkens it. hero.jpg is a
   dark storm/circuit image to begin with, so once three separate dark
   layers stacked over it (this scrim's two, plus .radar-vignette) it was
   effectively invisible -- the compounded coverage worked out to roughly
   52% at the very brightest point and over 90% toward the edges. Raising
   the image's own brightness/contrast/saturation means the scrims can
   still do their legibility job without erasing the photo. */
.hero .section-bg {
  filter: brightness(1.22) contrast(1.06) saturate(1.14);
}

/* The dark-fade layers (legibility) paint on top; the two color glows
   behind them are this section's gradient-mesh -- radar green echoing the
   radar sweep at top-right, a cool blue undertone opposite it for depth,
   both faint enough to read as ambient light rather than a color wash.

   Reworked so the darkening is CONCENTRATED, not global: the first
   gradient is now a tight pool sitting directly behind the centered
   headline block (where white text actually needs contrast) and fading
   to nothing well before the edges, instead of a section-wide radial
   that hit 0.88 opacity at 72% out. The linear layer keeps top/bottom
   falloff for the nav and scroll-cue but is much lighter through the
   middle band, where the photo's most interesting detail lives. */
.hero-scrim {
  background:
    radial-gradient(ellipse 54% 44% at 50% 52%, rgba(5, 7, 10, 0.82) 0%, rgba(5, 7, 10, 0.55) 52%, rgba(5, 7, 10, 0) 100%),
    linear-gradient(to bottom, rgba(5, 7, 10, 0.66) 0%, rgba(5, 7, 10, 0.2) 26%, rgba(5, 7, 10, 0.2) 66%, rgba(5, 7, 10, 0.84) 100%),
    radial-gradient(ellipse 60% 50% at 82% 10%, rgba(51, 255, 140, 0.2) 0%, transparent 60%),
    radial-gradient(ellipse 55% 45% at 6% 92%, rgba(37, 99, 235, 0.15) 0%, transparent 60%);
}

.radar {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2;
  /* One source of truth for the radar's diameter -- the rings SVG and the
     sweep must stay exactly the same size or the green sweep visibly
     overruns (or falls short of) the outermost ring. Sized past the
     viewport on purpose so the dish reads as something the page is
     sitting inside rather than a contained circle; .panel's overflow
     hidden does the cropping, and .radar-vignette softens where the
     rings run off-screen. */
  --radar-size: min(128vw, 1180px);
}

/* The third of the three layers that were burying the hero photo. Its
   real job is blending the radar rings' outer edge into the background
   so they don't just stop mid-air -- at 0.55/82% it was also crushing
   the photo everywhere outside the center. Pushed the fade start further
   out and cut the opacity, which still hides the rings' hard edge while
   leaving the photo's outer detail visible. */
.radar-vignette {
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse at center, transparent 0%, var(--bg-void) 94%);
  z-index: 2;
  opacity: 0.3;
}

.radar-rings {
  width: var(--radar-size);
  height: var(--radar-size);
  opacity: 0.7;
}

.radar-rings circle,
.radar-rings line {
  fill: none;
  stroke: rgba(255, 255, 255, 0.07);
  stroke-width: 1;
}

/* The single most literal "radar" moment on the page -- a real sweeping
   scan line, now in radar green instead of brand coral, matching an
   actual radar/sonar display instead of just being brand-colored. */
.radar-sweep {
  position: absolute;
  width: var(--radar-size);
  height: var(--radar-size);
  border-radius: 50%;
  /* Wedge widened along with the dish. At the old diameter a 14deg-to-50deg
     falloff read as a distinct blade; spread across a much larger circle the
     same angles would thin out into a faint smear, so the leading edge is
     held brighter and the tail given more room to fade. */
  background: conic-gradient(from 0deg, transparent 0deg, rgba(51, 255, 140, 0.28) 18deg, transparent 62deg);
  mix-blend-mode: screen;
  animation: sweep 11s linear infinite;
}

@keyframes sweep {
  to { transform: rotate(360deg); }
}

/* Rain sheets (weather effect 1/4, hero only) -- outer/inner split like the
   old orb spheres: .rain-layer is sized to the section and carries the
   scroll/pointer-drift transform (see [data-drift] in script.js);
   .rain-far/.rain-mid/.rain-near are three independent depth layers inside
   it, each its own tiled SVG streak texture, oversized (200% height) so a
   looping `transform: translateY` never exposes an edge.

   Depth comes from THREE things varying together across the three layers,
   not just one: tile size (far=smallest/densest through near=biggest/
   sparsest), opacity (far=faintest through near=brightest), and fall
   speed (far=slowest through near=fastest) -- the same size+speed logic
   real parallax depth uses generally, applied to raindrops specifically.
   Color leans toward the hero's own established coral/blue palette (see
   .hero-scrim's two glow colors) instead of plain white/grey -- far skews
   blue, near skews coral, mid mixes both.

   Each streak's stroke is itself an SVG linearGradient fading from
   transparent (tail) to its peak opacity (leading edge) along the drop's
   own line, rather than a flat-color stroke -- that gradient IS the
   "motion blur" look (a soft trailing fade instead of a crisp static
   dash), achieved as part of the static texture so it costs nothing extra
   per frame, rather than an animated CSS filter: blur() (which is far
   more likely to force a repaint on mobile every frame than a transform-
   only animation is).

   Deliberately transform-only for all three layers' animation -- no
   animated background-position, width, or filter -- so this stays on the
   GPU compositor thread instead of forcing a repaint every frame, since
   mobile performance here matters as much as it did for the trigger map's
   marker clustering. */
.rain-layer {
  position: absolute;
  inset: 0;
  z-index: 2;
  overflow: hidden;
  pointer-events: none;
  transform: translate(var(--drift-x, 0px), calc(var(--parallax-y, 0px) * 0.5));
}

.rain-far, .rain-mid, .rain-near {
  position: absolute;
  top: -50%;
  left: 0;
  width: 100%;
  height: 200%;
  background-repeat: repeat;
  will-change: transform;
}

/* All three layers share the SAME ~20-degree slant (dx/dy ~= 0.36) --
   rain in any given wind condition falls at one angle, so varying it
   per layer would read as three unrelated effects rather than one
   storm at three depths. An earlier pass used ~11 degrees, which at
   real rendering size read as near-vertical scratches rather than
   falling rain -- caught by inspecting a 3x-zoomed screenshot, since
   at 1x the difference is easy to miss entirely. */
.rain-far {
  opacity: 0.2;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='100'%3E%3Cdefs%3E%3ClinearGradient id='a' x1='12' y1='4' x2='23' y2='34' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%232563eb' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%232563eb' stop-opacity='0.5'/%3E%3C/linearGradient%3E%3ClinearGradient id='b' x1='40' y1='50' x2='49' y2='76' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%232563eb' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%232563eb' stop-opacity='0.35'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cline x1='12' y1='4' x2='23' y2='34' stroke='url(%23a)' stroke-width='1.1' stroke-linecap='round'/%3E%3Cline x1='40' y1='50' x2='49' y2='76' stroke='url(%23b)' stroke-width='1' stroke-linecap='round'/%3E%3C/svg%3E");
  background-size: 60px 100px;
  animation: rain-fall-far 2.2s linear infinite;
}

.rain-mid {
  opacity: 0.26;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='140'%3E%3Cdefs%3E%3ClinearGradient id='c' x1='14' y1='6' x2='33' y2='58' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23ff5d52' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23ff5d52' stop-opacity='0.55'/%3E%3C/linearGradient%3E%3ClinearGradient id='d' x1='60' y1='40' x2='77' y2='86' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%232563eb' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%232563eb' stop-opacity='0.42'/%3E%3C/linearGradient%3E%3ClinearGradient id='e' x1='80' y1='90' x2='92' y2='124' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23ff5d52' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23ff5d52' stop-opacity='0.32'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cline x1='14' y1='6' x2='33' y2='58' stroke='url(%23c)' stroke-width='1.5' stroke-linecap='round'/%3E%3Cline x1='60' y1='40' x2='77' y2='86' stroke='url(%23d)' stroke-width='1.3' stroke-linecap='round'/%3E%3Cline x1='80' y1='90' x2='92' y2='124' stroke='url(%23e)' stroke-width='1.1' stroke-linecap='round'/%3E%3C/svg%3E");
  background-size: 100px 140px;
  animation: rain-fall-mid 1.5s linear infinite;
}

.rain-near {
  opacity: 0.32;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='200'%3E%3Cdefs%3E%3ClinearGradient id='f' x1='24' y1='10' x2='54' y2='92' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23ff5d52' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23ff5d52' stop-opacity='0.7'/%3E%3C/linearGradient%3E%3ClinearGradient id='g' x1='95' y1='70' x2='121' y2='142' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0' stop-color='%23ff8a80' stop-opacity='0'/%3E%3Cstop offset='1' stop-color='%23ff8a80' stop-opacity='0.55'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cline x1='24' y1='10' x2='54' y2='92' stroke='url(%23f)' stroke-width='1.9' stroke-linecap='round'/%3E%3Cline x1='95' y1='70' x2='121' y2='142' stroke='url(%23g)' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-size: 150px 200px;
  animation: rain-fall-near 1s linear infinite;
}

@keyframes rain-fall-far { from { transform: translateY(0); } to { transform: translateY(100px); } }
@keyframes rain-fall-mid { from { transform: translateY(0); } to { transform: translateY(140px); } }
@keyframes rain-fall-near { from { transform: translateY(0); } to { transform: translateY(200px); } }
/* Each keyframe's end value is exactly its own layer's background-size
   height -- an exact, not approximate, seamless loop per layer. */

/* Hero's one deliberate glow lives in the sweep above — blips below are
   intentionally flat (just color) so they don't compete with it. */
.radar-blip {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: 7px;
  height: 7px;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  opacity: 0.8;
}

.blip-power { background: var(--data-power); }
.blip-flood { background: var(--data-flood); }
.blip-urgent { background: var(--data-urgent); }
.blip-freeze { background: var(--data-freeze); }
.blip-rain { background: var(--data-rain); }
.blip-storm { background: var(--data-storm); }

/* Soft shadow on every piece of hero copy, now that the photo behind it
   is genuinely visible rather than crushed to near-black. This is what
   lets the scrim stay light enough to keep the photo readable: legibility
   comes from a shadow hugging each glyph instead of from dimming the
   whole image. Costs the photo nothing, unlike a heavier scrim. */
.hero-content {
  position: relative;
  z-index: 3;
  max-width: 800px;
  margin: 0 auto;
  /* Bottom padding reserves the strip the absolutely-positioned
     .scroll-cue occupies. The panel centres this block, so the padding
     lifts the copy clear of the cue instead of letting the last line run
     into it -- which it did once the headline grew to five lines
     (measured: sources bottom 754 vs cue top 731 at a 799px-tall
     viewport, i.e. an ordinary laptop). Zeroed on mobile below, where the
     cue is hidden and the vertical budget is tighter. */
  padding: calc(var(--nav-height) + 24px) var(--edge-pad) 72px;
  text-align: center;
  text-shadow: 0 1px 3px rgba(5, 7, 10, 0.55), 0 2px 16px rgba(5, 7, 10, 0.75);
}

/* ---- Hero headline: chrome + storm-photo-in-text, level and centered ----
   Deliberately a different typographic register from the rest of the page
   (Anton, condensed heavy, uppercase) -- Inter still owns every other
   element. NOTE: keep `transform` off this element. .reveal/.reveal.is-visible
   animate transform on the h1, so anything static set here gets overwritten
   the moment the reveal fires -- that's why the (since-removed) perspective
   tilt needed its own wrapper span rather than living here. */
.hero h1 {
  font-family: "Anton", "Inter", system-ui, sans-serif;
  /* Full size on any normal-height window. An earlier version folded a
     vh term in here to prevent overflow on short laptops, but that term
     ended up governing everywhere and shrank the type on screens with
     plenty of room to spare (measured ~150px of unused vertical slack at
     an 860px viewport while the headline was being held to ~62px). The
     short-window case is handled by its own tier further down instead,
     which tightens the surrounding spacing as well -- much of the height
     pressure there is fixed padding/margins, not the headline. */
  font-size: clamp(30px, 5.1vw, 70px);
  font-weight: 400; /* Anton ships a single weight; its "boldness" is the face itself */
  /* Opened from 0.9. Anton has short descenders and tall caps, so a
     sub-1 line-height packs stacked all-caps lines almost edge to edge.
     Font-size came down a touch in the same change to pay for the extra
     leading -- five lines still have to clear the scroll cue. */
  line-height: 1.08;
  /* Anton is a condensed face with very tight default sidebearings --
     at display size its caps run together without help. Opened up well
     past the near-zero tracking a normal-width face would want. */
  letter-spacing: 0.05em;
  text-transform: uppercase;
  margin-bottom: 22px;
  /* Kill the inherited .hero-content text-shadow. With background-clip:text
     + transparent fill, a text-shadow paints the offset glyph silhouette
     OVER the clipped background and destroys the effect -- depth here has
     to come from filter: drop-shadow() instead, which applies to the
     already-composited result. */
  text-shadow: none;
}

.ht-chrome,
.ht-storm,
.ht-word {
  display: block;
}

.ht-storm,
.ht-word {
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent; /* Safari needs this explicitly; `color` alone is ignored there */
}

/* .ht-chrome is now only a container + the shared extrude/halo; the metal
   ramp itself lives on each .ht-word below. Keeping the filter here (not
   per word) means the extrude and halo are composited once across the
   whole clause rather than three times, and the stacked offsets stay
   consistent line to line. */
.ht-chrome {
  filter:
    drop-shadow(1px 1px 0 rgba(96, 110, 130, 0.85))
    drop-shadow(2px 2px 0 rgba(74, 86, 102, 0.7))
    drop-shadow(3px 3px 0 rgba(52, 61, 73, 0.55))
    /* Tight near-black halo: the reference sits on flat black, whereas
       this headline sits over a busy photo. The halo separates the
       letterforms from that photo locally, instead of dimming the whole
       image back down (which would undo the hero-photo work entirely). */
    drop-shadow(0 0 14px rgba(5, 7, 10, 0.95))
    drop-shadow(0 10px 30px rgba(5, 7, 10, 0.85))
    drop-shadow(0 0 34px rgba(168, 200, 255, 0.22));
}

/* Brushed-metal ramp: bright crown, a dark "horizon" band just above the
   optical centre, a second highlight below it, then falling off again --
   that double-highlight is what reads as metal rather than a flat grey
   gradient.

   Applied PER WORD, not once across the whole clause. Stretched over three
   stacked lines a single ramp would hand each word only a third of itself
   -- the middle word landing squarely in the dark horizon band and reading
   as a flat grey line between two bright ones. Per word, each gets the
   complete bright-dark-bright ramp, which is also how the reference's
   lines each read as their own piece of metal. */
.ht-word {
  background-image: linear-gradient(
    180deg,
    #ffffff 0%,
    #e4ebf6 26%,
    #93a1b8 46%,
    #5d6979 53%,
    #cbd7e8 64%,
    #ffffff 82%,
    #9fadc1 100%
  );
}

/* The real storm photo (lightning + cloud bank) showing through the
   letterforms. Applied to the whole block rather than per-line so the
   image runs CONTINUOUSLY across both wrapped lines -- a per-line
   background would restart the crop on each line and read as two
   unrelated textures. contrast/brightness lift the bolt so it survives
   being seen only through narrow glyph apertures. */
.ht-storm {
  /* Left to wrap naturally (deliberately NOT stacked per word like the
     chrome clause above), but balanced so the two lines come out even.
     Without this, the headline size reduction made it break as
     "BEFORE THE STORM / DOES." -- a one-word orphan. Balancing keeps the
     even "BEFORE THE / STORM DOES." split without hard-coding a break
     that would be wrong at other viewport widths. */
  text-wrap: balance;
  background-image: url("assets/img/bg/lifecycle-during.jpg");
  /* Zoomed past `cover` so the bolt and its branches fill the letterforms
     rather than the whole seascape being squeezed into them -- at plain
     cover the lightning was a thin thread and the glyphs mostly sampled
     flat cloud. */
  background-size: 150% auto;
  background-position: 40% 37%;
  /* Hard contrast push with NO brightness lift: the source region averages
     fairly bright (pale cloud), and lifting it further was exactly what
     made the first attempt read as washed-out lavender. Driving contrast
     instead darkens the cloud and drives the bolt toward white, which is
     what actually reads as "storm" through narrow glyph apertures. */
  filter:
    contrast(1.9) saturate(0.78) brightness(0.84)
    drop-shadow(0 0 14px rgba(5, 7, 10, 0.95))
    drop-shadow(0 8px 30px rgba(5, 7, 10, 0.85))
    drop-shadow(0 0 30px rgba(150, 190, 255, 0.3));
}

.hero-sub {
  font-size: clamp(15px, 1.6vw, 18px);
  line-height: 1.6;
  color: var(--text-secondary);
  max-width: 620px;
  margin: 0 auto 36px;
}

/* Buttons opt out of the inherited hero text-shadow above: .cta-primary
   is near-black text on coral, where a dark glow just muddies the label
   rather than lifting it off anything. */
.hero-actions .cta-primary,
.hero-actions .cta-secondary {
  text-shadow: none;
}

.hero-actions {
  display: flex;
  gap: 16px;
  justify-content: center;
  flex-wrap: wrap;
  margin-bottom: 28px;
}

/* Short desktop windows (laptops, a browser sharing the screen). Five
   stacked headline lines plus the eyebrow/subtitle/buttons/sources only
   overflow once the window gets short, so the squeeze lives here rather
   than in the base rule -- that keeps full-size type on every normal
   display. Most of the pressure at these heights is fixed padding and
   margins, not the headline, so those tighten alongside it; without that
   the headline alone would have to absorb the whole deficit and end up
   far smaller than necessary. */
@media (min-width: 781px) and (max-height: 820px) {
  .hero-content {
    padding-top: calc(var(--nav-height) + 12px);
    padding-bottom: 56px;
  }

  .hero h1 {
    font-size: clamp(30px, min(5.1vw, 8.2vh), 70px);
    margin-bottom: 16px;
  }

  .hero-sub { margin-bottom: 22px; }
  .hero-actions { margin-bottom: 18px; }
}

@media (max-width: 780px) {
  .hero-content {
    padding-top: calc(var(--nav-height) + 12px);
    padding-bottom: 0; /* .scroll-cue is hidden below this breakpoint, so nothing to reserve for */
  }

  /* The desktop rule's 5.1vw resolves to ~20px on a 390px-wide phone, so
     the clamp floor (30px) was what actually rendered -- tiny against the
     screen. A single vw factor genuinely can't serve both ends here: the
     headline is stacked one word per line, so its size is governed by the
     longest word spanning the CONTENT width, and that works out to ~19vw
     on phones vs ~5vw at desktop widths (measured: "MARKETING" in Anton
     with this tracking is 4.65x the font-size in width).

     Sized to ~80% of the full-width figure rather than 100%: five stacked
     lines at true full width don't leave room for the subtitle, buttons
     and source line within one 100dvh section, and this hero has to fit
     without internal scrolling (see the scroll-snap notes at the top).
     Leading also tightens back toward 1 here, since the extra desktop
     leading is multiplied across five lines on the smallest screens. */
  .hero h1 {
    /* Same width+height pairing as the desktop rule, just weighted for
       phones: 15vw is what makes the stacked words actually fill a narrow
       screen, and the 8.6vh term keeps five lines from crowding out the
       subtitle/buttons/sources on short viewports (a phone's usable dvh
       shrinks further whenever the browser chrome is showing). */
    font-size: clamp(34px, min(15vw, 8.6vh), 68px);
    line-height: 1.02;
    margin-bottom: 14px;
  }

  .hero-sub { margin-bottom: 20px; }
  .hero-actions { margin-bottom: 18px; gap: 10px; }

  /* Hidden below the breakpoint, because the bigger headline above now
     grows hero-content down into the cue's absolutely-positioned strip
     (measured: sources bottom 658 vs cue top 611 at the narrowest
     viewport testable here) -- they'd overlap. The cue is a desktop
     affordance anyway; a touch device doesn't need to be told the page
     scrolls. */
  .scroll-cue { display: none; }
}

.cta-primary, .cta-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
  font-weight: 600;
  padding: 14px 28px;
  border-radius: 999px;
  cursor: pointer;
  border: 1px solid transparent;
  box-shadow: 0 0 0 0 transparent;
  transition: transform 0.3s var(--ease-out-expo), box-shadow 0.4s var(--ease-standard),
    background 0.3s var(--ease-standard);
}

.cta-primary {
  background: var(--brand-coral);
  color: #140503;
}

.cta-primary:hover {
  transform: translateY(-1px);
  box-shadow: 0 0 26px 3px var(--brand-glow);
}

.cta-primary:active {
  transform: translateY(0) scale(0.97);
  box-shadow: 0 0 14px 1px var(--brand-glow);
}

.cta-secondary {
  background: rgba(255, 255, 255, 0.04);
  border-color: var(--border-soft);
  color: var(--text-primary);
}

.cta-secondary:hover { background: rgba(255, 255, 255, 0.09); }
.cta-secondary:active { transform: scale(0.97); }

.hero-sources {
  font-size: 12px;
  color: var(--text-tertiary);
  letter-spacing: 0.02em;
}

.scroll-cue {
  position: absolute;
  bottom: 28px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  background: none;
  border: none;
  color: var(--text-tertiary);
  cursor: pointer;
  padding: 8px;
  animation: bob 2.2s ease-in-out infinite;
}

@keyframes bob {
  0%, 100% { transform: translate(-50%, 0); }
  50% { transform: translate(-50%, 6px); }
}

/* ==========================================================================
   2. DEMO VIDEO
   ========================================================================== */

.demo { padding: calc(var(--nav-height) + 32px) var(--edge-pad) 48px; background: var(--bg-deep); }

/* Violet echoing orb-storm at top-left, a cool teal undertone bottom-right
   for balance -- distinct from the hero's coral/blue mix above. */
/* Drifting smoke (weather effect 3/4), demo section only.

   Each layer is oversized by exactly one tile on every side it travels,
   so a translate of exactly one tile never exposes an edge and lands the
   texture perfectly back on itself -- a seamless loop with no snap. The
   margins are written as `calc(100% + 2 * tile)` rather than a percentage
   like the rain layer uses, because a percentage margin stops being big
   enough once the tile is large relative to the section (which is exactly
   the case on a narrow phone).

   Transform-only animation on a static bitmap: the layer rasterises once
   and the drift is a GPU composite, rather than the per-frame repaint an
   animated background-position would cost. */
.smoke-layer {
  position: absolute;
  z-index: 1;
  pointer-events: none;
  background-repeat: repeat;
  will-change: transform;
}

/* Broad, billowy bank -- larger tile, slower, drifting up-left. */
.smoke-a {
  top: -360px;
  left: -360px;
  width: calc(100% + 720px);
  height: calc(100% + 720px);
  background-image: url("assets/img/smoke/smoke-a.png");
  background-size: 360px 360px;
  opacity: 0.15;
  animation: smoke-drift-a 78s linear infinite;
}

/* Finer wisps -- smaller tile, faster, crossing the other way so the two
   shear against each other instead of moving as one sheet. */
.smoke-b {
  top: -240px;
  left: -240px;
  width: calc(100% + 480px);
  height: calc(100% + 480px);
  background-image: url("assets/img/smoke/smoke-b.png");
  background-size: 240px 240px;
  opacity: 0.11;
  animation: smoke-drift-b 51s linear infinite;
}

/* End values are exactly one tile, which is what makes the loop seamless. */
@keyframes smoke-drift-a {
  from { transform: translate(0, 0); }
  to   { transform: translate(-360px, -360px); }
}

@keyframes smoke-drift-b {
  from { transform: translate(0, 0); }
  to   { transform: translate(240px, -240px); }
}

@media (max-width: 780px) {
  /* Smaller tiles on phones so the wisps stay at a believable scale
     relative to the screen -- at desktop tile sizes a single billow would
     span most of a phone display and read as a grey wash rather than
     smoke. Margins and keyframe distances shrink to match, since the
     seamless loop depends on them being exactly one tile. */
  .smoke-a {
    top: -220px; left: -220px;
    width: calc(100% + 440px); height: calc(100% + 440px);
    background-size: 220px 220px;
    animation-name: smoke-drift-a-sm;
  }

  .smoke-b {
    top: -150px; left: -150px;
    width: calc(100% + 300px); height: calc(100% + 300px);
    background-size: 150px 150px;
    animation-name: smoke-drift-b-sm;
  }
}

@keyframes smoke-drift-a-sm {
  from { transform: translate(0, 0); }
  to   { transform: translate(-220px, -220px); }
}

@keyframes smoke-drift-b-sm {
  from { transform: translate(0, 0); }
  to   { transform: translate(150px, -150px); }
}

.demo-section-scrim {
  background: linear-gradient(to bottom,
      rgba(9, 12, 17, 0.15) 0%, var(--bg-deep) 22%,
      var(--bg-deep) 78%, rgba(9, 12, 17, 0.15) 100%),
    radial-gradient(ellipse 55% 50% at 10% 4%, rgba(124, 58, 237, 0.18) 0%, transparent 62%),
    radial-gradient(ellipse 50% 45% at 92% 96%, rgba(8, 145, 178, 0.14) 0%, transparent 60%);
}

/* A clean outline around the footage itself -- no titlebar, no second border.
   .demo-frame-body is the media container holding the video/placeholder stack.

   The frame is locked to the video's own 16:9 ratio rather than stretched to
   fill whatever vertical gap the flex column leaves. It used to be `flex: 1`
   with only a max-height, which on a wide desktop produced a ~3.8:1 letterbox
   slot; `object-fit: cover` then had to crop the top and bottom off the
   picture to fill it, and on a phone cropped the sides instead. Sizing the box
   to the content's aspect means nothing is cropped and no black bars appear,
   at any viewport.

   Width is whichever limit binds first: the column, the readable 1080px cap,
   or the height budget converted back into a width. Expressing the height cap
   as a width is what keeps the ratio intact -- a plain `max-height` would
   squash the box and reintroduce the crop. */
.demo-frame {
  position: relative;
  z-index: 2;
  --demo-max-h: 56vh;
  width: min(100%, 1080px, calc(var(--demo-max-h) * 16 / 9));
  aspect-ratio: 16 / 9;
  margin: 0 auto;
  flex: none;
  background: #000;
  border: 1px solid var(--border-soft);
  border-radius: 14px;
  overflow: hidden;
  box-shadow: 0 30px 70px -20px rgba(0, 0, 0, 0.65), 0 0 90px -26px var(--brand-green-glow);
}

@supports (height: 1dvh) {
  .demo-frame { --demo-max-h: 56dvh; }
}

.demo-frame-body {
  position: relative;
  width: 100%;
  height: 100%;
  background: #000;
  overflow: hidden;
}

/* `contain` rather than `cover`: the box is already the video's exact ratio,
   so neither crops in practice -- but if a future viewport ever makes the two
   disagree, contain shows the whole frame instead of silently cutting it. */
.demo-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.demo-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  background: radial-gradient(ellipse at center, #12161f 0%, #000 75%);
  z-index: 2;
}

.play-glyph {
  width: 68px;
  height: 68px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  background: rgba(51, 255, 140, 0.16);
  border: 1px solid rgba(51, 255, 140, 0.5);
  box-shadow: 0 0 30px 4px var(--brand-green-glow);
}

.demo-caption {
  font-size: 13px;
  color: var(--text-tertiary);
  letter-spacing: 0.03em;
}

.demo-copy {
  position: relative;
  z-index: 2;
  max-width: 720px;
  margin: 40px auto 0;
  padding: 0 var(--edge-pad);
  text-align: center;
}

.demo-copy h2 { margin-bottom: 14px; }

.demo-copy p {
  color: var(--text-secondary);
  font-size: 15px;
  line-height: 1.6;
}

@media (max-width: 780px) {
  .demo { padding: calc(var(--nav-height) + 16px) var(--edge-pad) 20px; }
  /* On a phone the column width binds long before this does -- 16:9 inside a
     ~327px column is only ~184px tall. The cap is kept generous on purpose so
     width stays the governing constraint and the video is as large as it can
     be; the old 30dvh cap actively squashed the box and forced the crop. */
  .demo-frame { --demo-max-h: 46vh; border-radius: 12px; }
  .demo-copy { margin-top: 18px; }
  .demo-copy h2 { margin-bottom: 8px; }
}

@supports (height: 1dvh) {
  @media (max-width: 780px) {
    .demo-frame { --demo-max-h: 46dvh; }
  }
}

/* ==========================================================================
   3. BEFORE / DURING / AFTER
   ========================================================================== */

.lifecycle {
  background: var(--bg-void);
  padding: calc(var(--nav-height) + 40px) 0 48px;
}

/* Three glows left-to-right echo the phase order beneath (before=blue,
   during=red, after=grey -- see --phase-color on .phase-before/during/after)
   rather than one flat dark fade, hinting at the section's own before/
   during/after structure before you even reach the cards. */
.lifecycle-section-scrim {
  background: radial-gradient(ellipse at 50% 0%, rgba(5, 7, 10, 0.75) 0%, rgba(5, 7, 10, 0.94) 60%),
    radial-gradient(ellipse 42% 55% at 12% 55%, rgba(37, 99, 235, 0.14) 0%, transparent 65%),
    radial-gradient(ellipse 42% 55% at 50% 62%, rgba(220, 38, 38, 0.11) 0%, transparent 65%),
    radial-gradient(ellipse 42% 55% at 88% 55%, rgba(107, 114, 128, 0.13) 0%, transparent 65%),
    var(--bg-void);
}

/* Lightning (weather effect 2/4), lifecycle section only. z-index 1 puts
   it above .section-bg (0) and level with .section-scrim, but it appears
   after the scrim in the markup so it paints on top of it; the cards and
   section heading sit at z-index 2 and stay above the bolts.

   stroke-dashoffset is set from JS per frame (see initLightning) rather
   than animated in CSS, because the whole point is that it tracks scroll
   position instead of running on a timer. No CSS transition on it either
   -- a transition would lag behind and smear the draw during fast
   scrolling instead of following the finger. */
.lightning-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
}

/* Pure white core with a heavy multi-radius green bloom. An earlier pass
   used a dimmer off-white at 0.85 opacity with a soft glow, which on a
   phone -- where the stroke is only ~2px and the glow spreads over far
   fewer pixels -- just read as grey sticks. Brightness here has to come
   from the CORE being blown out and the bloom being genuinely intense,
   not from a wide faint halo, which is what disappears at small sizes. */
.bolt {
  fill: none;
  stroke: #ffffff;
  stroke-width: 2.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  filter:
    drop-shadow(0 0 2px #ffffff)
    drop-shadow(0 0 5px rgba(51, 255, 140, 1))
    drop-shadow(0 0 14px rgba(51, 255, 140, 0.9))
    drop-shadow(0 0 34px rgba(51, 255, 140, 0.55));
  /* The filter above is static, so the browser can rasterise the glowing
     bolt once and then treat the flicker as a cheap layer-opacity change
     rather than re-running four drop-shadows every frame. */
  will-change: opacity;
  animation: bolt-flicker 5.5s linear infinite;
}

/* Irregular on purpose -- evenly spaced pulses read as a blinking LED.
   Real arcs are unstable: long steady stretches broken by bursts of rapid
   flutter. Held mostly-on so the bolt never disappears out from under a
   reader mid-sentence. */
@keyframes bolt-flicker {
  0%, 11%   { opacity: 0.92; }
  12%       { opacity: 0.34; }
  13%       { opacity: 1; }
  15%       { opacity: 0.48; }
  16%, 43%  { opacity: 0.95; }
  44%       { opacity: 0.55; }
  45%       { opacity: 1; }
  46%       { opacity: 0.4; }
  48%, 71%  { opacity: 0.9; }
  72%       { opacity: 1; }
  74%       { opacity: 0.62; }
  76%, 100% { opacity: 0.92; }
}

/* Offsets so the bolts never flicker in lockstep -- simultaneous flutter
   across every bolt reads as the whole layer blinking rather than as
   separate arcs. */
.lightning-layer .bolt:nth-child(2) { animation-delay: -1.7s; }
.lightning-layer .bolt:nth-child(3) { animation-delay: -3.1s; }
.lightning-layer .bolt:nth-child(4) { animation-delay: -0.8s; }
.lightning-layer .bolt:nth-child(5) { animation-delay: -4.3s; }

/* The storm lighting the room up. Opacity-only animation on a plain
   gradient, so it composites cheaply -- deliberately NOT a filter or
   background-position animation, which would repaint every frame. */
.sky-flash {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  will-change: opacity;
}

.sky-flash-a {
  background: radial-gradient(ellipse 70% 55% at 20% 22%, rgba(51, 255, 140, 0.26) 0%, rgba(51, 255, 140, 0.07) 38%, transparent 70%);
  animation: sky-flash 7.3s ease-out infinite;
}

.sky-flash-b {
  background: radial-gradient(ellipse 65% 50% at 82% 32%, rgba(190, 255, 220, 0.2) 0%, rgba(51, 255, 140, 0.06) 40%, transparent 72%);
  animation: sky-flash 11.1s ease-out infinite;
  animation-delay: -3.6s;
}

/* A real strike is a double-tap: a hard leader flash, a near-instant
   collapse, then a weaker return stroke a beat later. Most of the cycle
   is dark, which is what keeps it feeling like weather instead of a
   pulsing glow. The two flashes use co-prime-ish periods (7.3s / 11.1s)
   so their pattern doesn't visibly repeat. */
@keyframes sky-flash {
  0%          { opacity: 0; }
  1%          { opacity: 1; }
  4%          { opacity: 0.08; }
  6%          { opacity: 0.72; }
  11%         { opacity: 0; }
  100%        { opacity: 0; }
}

/* Branches read as secondary via colour and a lighter bloom, NOT opacity:
   the flicker animation on .bolt animates opacity, so any static opacity
   declared here is simply overridden and does nothing. */
.bolt-branch {
  stroke-width: 1.6;
  stroke: #d6ffe8;
  filter:
    drop-shadow(0 0 2px rgba(255, 255, 255, 0.8))
    drop-shadow(0 0 6px rgba(51, 255, 140, 0.75))
    drop-shadow(0 0 18px rgba(51, 255, 140, 0.4));
}

/* Only one of the two layers is ever rendered; each is authored for the
   aspect ratio it actually gets. Note the hidden one's paths are still in
   the document, so getTotalLength() keeps working on them and they stay
   correctly initialised if the viewport crosses the breakpoint. */
.lightning-narrow { display: none; }

@media (max-width: 780px) {
  .lightning-wide { display: none; }
  .lightning-narrow { display: block; }

  /* Do NOT thin these down on mobile. A previous pass dropped the stroke
     to 1.9 at 0.75 opacity on the theory that the narrow viewBox scales
     ~1:1 and would otherwise render heavier -- the actual result was grey
     sticks, because a thin dim line surrounded by a glow spread across
     only a handful of physical pixels loses the colour entirely and keeps
     just the desaturated core. The bolt needs to stay bright and the
     bloom needs to stay tight for the green to survive at this size. */
  .bolt { stroke-width: 2.4; }
  .bolt-branch { stroke-width: 1.7; }

  /* Flash is stronger here too: less screen area means less total glow,
     so the same alpha reads much weaker than it does on desktop. */
  .sky-flash-a { background: radial-gradient(ellipse 95% 45% at 25% 24%, rgba(51, 255, 140, 0.34) 0%, rgba(51, 255, 140, 0.09) 40%, transparent 72%); }
  .sky-flash-b { background: radial-gradient(ellipse 90% 42% at 78% 34%, rgba(190, 255, 220, 0.26) 0%, rgba(51, 255, 140, 0.08) 42%, transparent 74%); }
}

.phase-grid {
  position: relative;
  z-index: 2;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 20px;
  max-width: 1180px;
  margin: 0 auto;
  padding: 0 var(--edge-pad);
}

.phase-card {
  position: relative;
  background: var(--bg-panel);
  border: 1px solid var(--border-soft);
  border-radius: 16px;
  overflow: hidden;
  transition: transform 0.4s var(--ease-out-expo), border-color 0.4s var(--ease-standard);
}

.phase-card:hover { transform: translateY(-3px); }

.phase-card-bg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}

.phase-card-scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top, rgba(6, 8, 12, 0.96) 0%, rgba(6, 8, 12, 0.8) 45%, rgba(6, 8, 12, 0.42) 100%);
}

.phase-card-content {
  position: relative;
  z-index: 2;
  padding: 32px 28px 36px;
  min-height: 340px;
  display: flex;
  flex-direction: column;
}

.phase-card::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 2px;
  z-index: 3;
  background: var(--phase-color);
}

.phase-before { --phase-color: var(--data-flood); }
.phase-during { --phase-color: var(--data-urgent); }
.phase-after { --phase-color: var(--data-after); }

/* The section's one accent glow: only the peak-intent "during" card lights up. */
.phase-during::before {
  box-shadow: 0 0 16px 1px var(--phase-color);
}

.phase-tag {
  display: inline-block;
  align-self: flex-start;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--phase-color);
  border: 1px solid var(--phase-color);
  border-radius: 999px;
  padding: 4px 12px;
  margin-bottom: 20px;
  background: rgba(5, 7, 10, 0.5);
  backdrop-filter: blur(4px);
}

.phase-card h3 {
  font-size: 19px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: 0;
  margin-bottom: 10px;
}

.phase-card p { margin-top: auto; padding-top: 12px; }

.phase-card p {
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--text-secondary);
}

@media (max-width: 900px) {
  .phase-grid { grid-template-columns: 1fr; }
}

/* Mobile: the three phase cards stack and scroll VERTICALLY with the rest
   of the page. This previously used a horizontal swipe carousel, on the
   reasoning that three photo+text cards can't fit one phone screen
   stacked -- true, but the section doesn't have to fit one screen. .panel
   is min-height:100dvh (not a fixed height), so it simply grows to hold
   the stack, and mobile scroll-snap is already `proximity` rather than
   `mandatory` specifically so a taller-than-viewport section can be
   scrolled through normally instead of being yanked to the next snap
   point. Centering stays safe for the same reason: a flex container only
   traps overflow above its top edge when its height is FIXED, and this
   one grows.

   Every carousel-specific property is explicitly reset rather than just
   dropped, since the desktop rules they were overriding (display:grid,
   the 900px single-column rule) are still in play at this width. */
@media (max-width: 780px) {
  .lifecycle { padding: calc(var(--nav-height) + 24px) 0 32px; }

  .section-head { margin-bottom: 28px; }

  .phase-grid {
    display: grid;
    grid-template-columns: 1fr;
    overflow-x: visible;
    scroll-snap-type: none;
    gap: 16px;
    padding: 0 var(--edge-pad);
  }

  .phase-card {
    flex: none;
    scroll-snap-align: none;
  }

  .phase-card-content {
    min-height: 0;
    padding: 22px 20px 24px;
  }

  .phase-tag { margin-bottom: 14px; }
  .phase-card h3 { font-size: 17px; }
  .phase-card p { padding-top: 8px; }
}

/* ==========================================================================
   4. REAL DASHBOARD
   ========================================================================== */

.dashboard { padding: 0; justify-content: flex-end; background: #000; }

.dashboard-shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* A single restrained amber glow (echoing orb-heat) top-left, well clear
   of where .dashboard-copy's own text sits -- this section's whole point
   is the real screenshot underneath, so unlike the other sections this
   stays closer to a legibility scrim than a color wash. */
.dashboard-scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(3, 4, 6, 0.97) 0%, rgba(3, 4, 6, 0.8) 45%, rgba(3, 4, 6, 0.25) 78%, transparent 100%),
    radial-gradient(ellipse 45% 38% at 14% 8%, rgba(245, 158, 11, 0.13) 0%, transparent 60%);
}

/* Desktop-only half-screen split: map on the left, concept popup filling
   the right half. Below the breakpoint this collapses back to a full-width
   map with .dashboard-proof-mobile handling the popup instead (see below) --
   a real half/half split makes no sense once "half" is ~185px wide. */
.dashboard-proof { display: none; }

@media (min-width: 781px) {
  .dashboard-shot {
    width: 55%;
    object-position: left center;
  }

  .dashboard-scrim { width: 55%; }

  .dashboard-copy { max-width: min(640px, 46vw); }

  .dashboard-proof {
    display: flex;
    flex-direction: column;
    position: absolute;
    z-index: 3;
    top: 0;
    right: 0;
    bottom: 0;
    left: 55%;
    margin: 0;
    border-radius: 0;
    border: none;
    border-left: 1px solid var(--border-soft);
    box-shadow: -24px 0 60px -20px rgba(0, 0, 0, 0.55);
    background: var(--bg-deep);
  }

  /* object-fit:contain (on .browser-frame img, see the shared mockup-chrome
     block above) was chosen over cover because the screenshot's own
     proportions (~1:1) never match a tall right-half panel at any
     realistic viewport -- contain + centered, framed on the browser
     frame's own dark background, always shows the complete image. */
  .dashboard-proof-media {
    flex: 1;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 36px 36px 24px;
    overflow: hidden;
  }

  .dashboard-proof figcaption { flex-shrink: 0; }
}

/* Mobile gets its own tighter crop (title + 2 full cards, no dimmed
   sidebar/map) placed inline in the copy flow instead of floating --
   the full desktop composite would shrink past legible at phone width.
   Framing itself (bezel/notch/shadow) lives on .device-frame now, not
   this wrapper -- see the shared mockup-chrome block above. */
.dashboard-proof-mobile {
  display: none;
  margin: 22px 0 0;
}

.dashboard-proof figcaption,
.dashboard-proof-mobile figcaption {
  padding: 18px 24px 22px;
  background: var(--bg-panel);
  border-top: 1px solid var(--border-soft);
}

@media (max-width: 780px) {
  .dashboard-proof-mobile figcaption { padding: 12px 16px 14px; }
}

.dashboard-proof figcaption .proof-tag,
.dashboard-proof-mobile figcaption .proof-tag {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--brand-green);
  margin-bottom: 6px;
}

.dashboard-proof figcaption p,
.dashboard-proof-mobile figcaption p {
  font-size: 14.5px;
  line-height: 1.5;
  color: var(--text-primary);
}

@media (max-width: 780px) {
  .dashboard-proof-mobile { display: block; }
}

.dashboard-copy {
  position: relative;
  z-index: 2;
  max-width: 640px;
  padding: 32px var(--edge-pad) 64px;
  /* Scaled to this block's own height, not the viewport's -- the old
     section-wide scrim assumed a fixed content height and silently lost
     coverage whenever this block grew taller (e.g. the mobile proof card
     appended below the legend). Self-sizing avoids that failure mode. */
  background: linear-gradient(to bottom, rgba(5, 7, 10, 0) 0%, rgba(5, 7, 10, 0.9) 18%, rgba(5, 7, 10, 0.9) 100%);
  border-radius: 20px 20px 0 0;
}

.dashboard-copy h2 { margin-bottom: 14px; }

.dashboard-copy p {
  color: var(--text-secondary);
  font-size: 15px;
  line-height: 1.6;
  margin-bottom: 22px;
}

@media (max-width: 780px) {
  .dashboard-copy { padding: 12px var(--edge-pad) 16px; }
  .dashboard-copy h2 { margin-bottom: 6px; }
  .dashboard-copy p { margin-bottom: 10px; }
  .legend { gap: 6px 14px; }
  .dashboard-proof-mobile { margin-top: 12px; width: 80%; margin-left: auto; margin-right: auto; }
}

.legend {
  list-style: none;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 20px;
}

.legend li {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--text-secondary);
}

.legend .dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  display: inline-block;
}

/* ==========================================================================
   5. CLOSING CTA + REQUEST ACCESS
   ========================================================================== */

.access {
  background: var(--bg-void);
  padding: calc(var(--nav-height) + 40px) var(--edge-pad) 24px;
  justify-content: space-between;
}

/* Cyan glow (freeze-themed, matching this section's original accent) --
   a quieter closing note than the busier sections above, fitting for the
   last, form-focused section. */
.access-section-scrim {
  background: radial-gradient(ellipse at 50% 0%, rgba(18, 22, 31, 0.55) 0%, rgba(5, 7, 10, 0.93) 55%),
    linear-gradient(to bottom, transparent 0%, var(--bg-void) 92%),
    radial-gradient(ellipse 50% 45% at 85% 66%, rgba(14, 165, 233, 0.15) 0%, transparent 60%);
}

.access-inner {
  position: relative;
  z-index: 2;
  max-width: 640px;
  margin: 0 auto;
  text-align: center;
  width: 100%;
}

.access h2 { margin-bottom: 16px; }

.access-sub {
  color: var(--text-secondary);
  font-size: 15px;
  line-height: 1.6;
  margin-bottom: 36px;
}

.access-form {
  display: flex;
  flex-direction: column;
  gap: 16px;
  text-align: left;
}

.field-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}

/* Honeypot field for access-form -- a real visitor never sees or fills
   this (off-screen, not display:none/visibility:hidden, since some bots
   skip those specifically but still auto-fill anything with a normal
   layout). If it comes back non-empty, the server treats the submission
   as spam. Never remove the tabindex/autocomplete attributes on the
   input itself (see index.html) -- they keep real keyboard users and
   password managers from ever landing in it. */
.hp-field {
  position: absolute;
  left: -9999px;
  top: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

@media (max-width: 600px) {
  .field-row { grid-template-columns: 1fr; }
}

.access-form label {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--text-secondary);
  letter-spacing: 0.02em;
}

.access-form input,
.access-form select,
.access-form textarea {
  appearance: none;
  -webkit-appearance: none;
  background: var(--bg-panel);
  border: 1px solid var(--border-soft);
  border-radius: 10px;
  padding: 12px 14px;
  font-size: 14px;
  color: var(--text-primary);
  outline: none;
  box-shadow: 0 0 0 0 transparent;
  transition: border-color 0.3s var(--ease-standard), box-shadow 0.3s var(--ease-standard);
  resize: vertical;
}

.access-form input::placeholder,
.access-form textarea::placeholder {
  color: var(--text-tertiary);
  opacity: 1;
}

.access-form input:hover,
.access-form select:hover,
.access-form textarea:hover {
  border-color: rgba(255, 255, 255, 0.18);
}

.access-form input:focus,
.access-form select:focus,
.access-form textarea:focus {
  border-color: var(--brand-green);
  box-shadow: 0 0 0 3px rgba(51, 255, 140, 0.15);
}

/* Dark-theme fix: Chrome forces a bright autofill background otherwise. */
.access-form input:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 1000px var(--bg-panel) inset;
  -webkit-text-fill-color: var(--text-primary);
  transition: background-color 9999s ease-in-out 0s;
}

.access-form select {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M6 9l6 6 6-6' fill='none' stroke='%239aa4b6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 14px center;
  background-size: 16px;
  padding-right: 40px;
}

.access-form select:invalid { color: var(--text-tertiary); }

.access-form button.cta-primary {
  margin-top: 8px;
  border: none;
  width: 100%;
}

.access-success {
  text-align: center;
  padding: 40px 20px;
  border: 1px solid var(--border-soft);
  border-radius: 16px;
  background: var(--bg-panel);
}

.access-success h3 { font-size: 20px; margin-bottom: 10px; }
.access-success p { color: var(--text-secondary); font-size: 14px; }

.access-error {
  text-align: center;
  font-size: 13px;
  color: var(--data-urgent);
  margin-top: 4px;
}

.site-footer {
  position: relative;
  z-index: 2;
  max-width: 640px;
  margin: 36px auto 0;
  width: 100%;
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: var(--text-tertiary);
  padding-top: 20px;
  border-top: 1px solid var(--border-softer);
}

@media (max-width: 780px) {
  .access { padding: calc(var(--nav-height) + 12px) var(--edge-pad) 12px; }
  .access h2 { margin-bottom: 10px; }
  .access-sub { margin-bottom: 14px; }
  .access-form { gap: 8px; }
  .field-row { gap: 8px; }
  .access-form label { gap: 4px; }
  .access-form input,
  .access-form select,
  .access-form textarea { padding: 10px 12px; }
  .access-form button.cta-primary { margin-top: 4px; }
  .site-footer { margin-top: 16px; padding-top: 12px; }
}

/* ==========================================================================
   Reduced motion
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .brand-dot, .radar-sweep, .scroll-cue, .rain-far, .rain-mid, .rain-near,
  .bolt, .sky-flash, .smoke-layer {
    animation: none !important;
  }

  /* Flicker keyframes held the bolts' opacity; with the animation off they
     need it set explicitly or they'd fall back to fully opaque and lose
     the intended weight. The flashes stay hidden entirely -- a strobing
     room is exactly what reduced-motion is asking us not to do. */
  .bolt { opacity: 0.92; }
  .sky-flash { opacity: 0; }

  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}
