/* ShotDetect — the brand layer shared by index.html and early-access.html.

   The palette is not invented: the deep navy field and the cyan pulse ring are
   the app's own artwork, and #2B62FF is the launcher icon's blue. What is new
   is the grade between them. A gunshot is one event that travels outward and
   changes state as it goes — heard, confirmed, located, warned — so the accents
   run as a single spectrum, cyan -> blue -> violet -> flare, and every gradient
   on the site is a slice of that same ramp. Flare (the magenta) is the alert end
   of it and is spent once per page, never as decoration.

   Type does brand work here rather than delivering text. Archivo carries the
   display line at an expanded width and a heavy weight, because the product is
   about something spreading out across a space. Instrument Sans sets body.
   IBM Plex Mono sets labels, because the app itself labels its sections that
   way (FAMILY, PAIRED PHONES, TIMELINE) and the tie is to the product.

   Page-specific layout stays in the page. What lives here is anything two pages
   would otherwise define twice and let drift: tokens, the top bar, buttons,
   store badges, the footer, the ambient field, and the motion system.

   policy.css is the separate, older stylesheet for the five legal documents.
   None of them load this file. */

@import url('https://fonts.googleapis.com/css2?family=Archivo:wdth,wght@100..125,400..900&family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Sans:wght@400..700&display=swap');

:root{
  /* Field */
  --abyss:#04091C;
  --deep:#070F2E;
  --navy:#0A1B4A;
  --royal:#123A96;

  /* The spectrum. In order: listening, networked, resolving, alerting. */
  --pulse:#22D3E0;
  --signal:#2B62FF;
  --violet:#7B3BFF;
  --flare:#FF2D74;
  --amber:#F0913F;

  --spectrum:linear-gradient(100deg,var(--pulse) 0%,var(--signal) 38%,var(--violet) 70%,var(--flare) 100%);

  --white:#FFFFFF;
  --mist:rgba(255,255,255,.68);
  --faint:rgba(255,255,255,.44);
  --hair:rgba(255,255,255,.13);

  --paper:#F4F7FC;
  --card:#FFFFFF;
  --ink:#0A1224;
  --ink-soft:#5A6478;
  --ink-hair:#E2E8F3;

  --display:"Archivo",-apple-system,BlinkMacSystemFont,"Segoe UI",Arial,sans-serif;
  --sans:"Instrument Sans",-apple-system,BlinkMacSystemFont,"Segoe UI Variable Text","Segoe UI",Inter,Roboto,"Helvetica Neue",Arial,sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Mono",Menlo,Consolas,monospace;

  --ease:cubic-bezier(.22,.61,.36,1);
  --ease-out:cubic-bezier(.16,1,.3,1);

  --pad:clamp(20px,5vw,48px);
  --shell:1160px;
  --badge-h:40px;
}

*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{
  margin:0;background:var(--abyss);color:var(--white);
  font-family:var(--sans);font-size:17px;line-height:1.6;
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  overflow-x:hidden;
}
.shell{max-width:var(--shell);margin:0 auto;padding:0 var(--pad);position:relative}
a{color:inherit}
:focus-visible{outline:2px solid var(--pulse);outline-offset:3px;border-radius:4px}

/* Display type. font-stretch is the reason Archivo is here rather than any
   other grotesque: the headline is set wide, which is a different silhouette
   from the condensed-heavy look every other product page arrives at. */
h1,h2,.display{
  font-family:var(--display);font-stretch:112%;font-weight:800;
  letter-spacing:-.038em;line-height:1.02;margin:0;
}
h3{font-family:var(--display);font-stretch:106%;font-weight:700;letter-spacing:-.022em;margin:0}

/* ---------- Ambient field ----------
   Three slow colour bodies plus a fine engineering grid. This is the grade the
   rest of the page sits inside; it is decorative, sits behind everything, and
   is switched off entirely under reduced motion. */
.field{position:absolute;inset:0;overflow:hidden;pointer-events:none;z-index:0}
.field::before,.field::after{content:"";position:absolute}
.field::before{
  inset:-24%;
  background:
    radial-gradient(46% 44% at 74% 30%,rgba(43,98,255,.55),transparent 62%),
    radial-gradient(38% 40% at 18% 12%,rgba(34,211,224,.24),transparent 64%),
    radial-gradient(44% 46% at 52% 88%,rgba(123,59,255,.30),transparent 66%);
  animation:drift 26s ease-in-out infinite alternate;
  filter:blur(6px);
}
.field::after{
  inset:0;
  background-image:linear-gradient(var(--hair) 1px,transparent 1px),
                   linear-gradient(90deg,var(--hair) 1px,transparent 1px);
  background-size:66px 66px;opacity:.34;
  mask-image:radial-gradient(72% 62% at 58% 42%,#000 16%,transparent 76%);
  -webkit-mask-image:radial-gradient(72% 62% at 58% 42%,#000 16%,transparent 76%);
}
@keyframes drift{
  0%  {transform:translate3d(0,0,0) scale(1)}
  50% {transform:translate3d(-3%,2%,0) scale(1.08)}
  100%{transform:translate3d(3%,-2%,0) scale(1.03)}
}
/* Anything that must sit above the field. */
.over{position:relative;z-index:1}

/* ---------- Eyebrow ---------- */
.eyebrow{
  font-family:var(--mono);font-size:11.5px;letter-spacing:.19em;text-transform:uppercase;
  color:var(--pulse);margin:0 0 20px;display:inline-flex;align-items:center;gap:10px;
}
.eyebrow::before{
  content:"";width:7px;height:7px;border-radius:50%;background:var(--pulse);flex:none;
  box-shadow:0 0 0 0 rgba(34,211,224,.65);animation:beacon 2.6s ease-out infinite;
}
@keyframes beacon{
  0%{box-shadow:0 0 0 0 rgba(34,211,224,.55)}
  70%{box-shadow:0 0 0 11px rgba(34,211,224,0)}
  100%{box-shadow:0 0 0 0 rgba(34,211,224,0)}
}

/* ---------- Top bar ---------- */
.top{
  position:sticky;top:0;z-index:40;
  background:rgba(4,9,28,.72);backdrop-filter:blur(16px);
  border-bottom:1px solid var(--hair);
}
.top-in{display:flex;align-items:center;justify-content:space-between;gap:18px;padding:14px 0}
.brand{display:flex;align-items:center;gap:11px;text-decoration:none}
.brand svg{width:26px;height:26px;flex:none}
.brand b{font-family:var(--display);font-stretch:108%;font-size:16.5px;font-weight:750;letter-spacing:-.02em}
/* The mark's trace draws itself once on load, as though the page had just heard
   something. 156 is the path length rounded up: getTotalLength() reports 155.37.
   It was 148 first, which is not a rounding error you can ignore — a dasharray
   shorter than the path leaves the last stroke permanently undrawn, so the
   waveform's trailing tail simply never appeared. */
.brand .trace{stroke-dasharray:156;stroke-dashoffset:156;animation:trace 1.5s var(--ease-out) .15s forwards}
@keyframes trace{to{stroke-dashoffset:0}}
.badges{display:flex;gap:9px;align-items:center}

/* Official store badge artwork, downloaded from Google and Apple and served
   unmodified from /badges. Both vendors require their own asset used as-is.

   The two assets are not built to the same spec, so they cannot share a
   height. Google ships a 646x250 canvas whose visible badge occupies the
   middle 564x168 — the 41px surround is the clear space its guidelines
   require, so the file is left intact and simply scaled up until the visible
   part matches Apple's. 168/250 = 0.672, hence the divisor. Apple's SVG is
   tight to the artwork, so its clear space is added as margin instead.

   NOTE: both vendors also require the badge to link to a live listing on their
   own store. Neither listing exists yet, so the Play badge points at the early
   access page. Repoint it at the listing the day there is one. */
.badge{display:block;line-height:0;border-radius:7px;transition:transform .2s var(--ease),opacity .2s ease}
.badge img{display:block;width:auto}
.badge-play img{height:calc(var(--badge-h) / .672)}
.badge-apple img{height:var(--badge-h);margin:calc(var(--badge-h) * .12)}
a.badge:hover{transform:translateY(-2px)}
.badge[aria-disabled="true"]{opacity:.42;cursor:default}

/* At phone width the wordmark plus two badges is wider than the viewport, and
   the bar is sticky so it cannot be allowed to wrap onto a second row. The
   mark alone carries the brand there, which buys enough room to keep both
   badges rather than dropping one. */
@media (max-width:600px){
  .top-in{gap:12px}
  /* Scoped to the bar: the footer uses .brand too, and has all the room it
     needs for the wordmark. */
  .top .brand b{display:none}
  :root{--badge-h:30px}
}

/* ---------- Buttons ----------
   The primary button carries the full spectrum and a light sweep that crosses
   it on hover — the same left-to-right travel as everything else on the site. */
.btn{
  position:relative;overflow:hidden;isolation:isolate;
  display:inline-flex;align-items:center;gap:9px;text-decoration:none;
  font-family:var(--sans);font-weight:650;font-size:16px;letter-spacing:-.012em;
  border-radius:13px;padding:16px 28px;border:1px solid transparent;
  transition:transform .18s var(--ease),box-shadow .18s ease,background .18s ease;
}
.btn svg{width:17px;height:17px;flex:none;transition:transform .22s var(--ease)}
.btn:hover svg{transform:translateX(3px)}
.btn-primary{
  background:var(--spectrum);background-size:180% 100%;background-position:0% 50%;
  color:#03122B;box-shadow:0 12px 34px -14px rgba(34,211,224,.8);
  transition:transform .18s var(--ease),box-shadow .18s ease,background-position .5s var(--ease);
}
.btn-primary:hover{
  transform:translateY(-2px);background-position:60% 50%;
  box-shadow:0 18px 44px -14px rgba(123,59,255,.85);
}
.btn-primary::after{
  content:"";position:absolute;inset:0;z-index:-1;
  background:linear-gradient(100deg,transparent 30%,rgba(255,255,255,.55) 50%,transparent 70%);
  transform:translateX(-120%);
}
.btn-primary:hover::after{animation:sheen .7s var(--ease) forwards}
@keyframes sheen{to{transform:translateX(120%)}}
.btn-ghost{border-color:var(--hair);color:var(--white);background:rgba(255,255,255,.03)}
.btn-ghost:hover{background:rgba(255,255,255,.08);transform:translateY(-2px)}

/* ---------- Wavefield ----------
   The signature, shared because both pages open with it: a shot at the centre,
   wavefronts leaving it, and nearby phones igniting in the order the front
   reaches them. Markup lives in each page; the timing lives here so the two can
   never fall out of step.

   Every ring runs on the same 4.8s bar, and each node fires on that bar at the
   delay its distance implies. What is drawn is therefore the thing the product
   actually does — one event, several arrival times — rather than three lights
   blinking. Per-element offsets are set in the page with --d. */
.wf-ring{transform-origin:center;animation:wf-ring 4.8s var(--ease) infinite;animation-delay:var(--d,0s)}
@keyframes wf-ring{
  0%  {transform:scale(.10);opacity:0}
  8%  {opacity:.8}
  100%{transform:scale(1);opacity:0}
}
.wf-core{transform-origin:center;animation:wf-core 4.8s var(--ease) infinite}
@keyframes wf-core{
  0%,100%{transform:scale(1);opacity:.9}
  4%     {transform:scale(1.5);opacity:1}
  16%    {transform:scale(1);opacity:.9}
}
.wf-node{opacity:.34;transform-origin:center;animation:wf-node 4.8s ease-out infinite;animation-delay:var(--d,0s)}
@keyframes wf-node{
  0%,100%{opacity:.34;filter:none}
  4%     {opacity:1;filter:drop-shadow(0 0 10px rgba(34,211,224,.9))}
  30%    {opacity:.34;filter:none}
}
.wf-sweep{transform-origin:center;animation:wf-sweep 11s linear infinite}
@keyframes wf-sweep{to{transform:rotate(360deg)}}
/* The line drawn between phones once they agree: it writes itself on the same
   bar, after the last node has lit. --len is the path length, set in the page. */
.wf-link{
  stroke-dasharray:var(--len,300);stroke-dashoffset:var(--len,300);
  animation:wf-link 4.8s var(--ease) infinite;animation-delay:var(--d,0s);
}
@keyframes wf-link{
  0%,16% {stroke-dashoffset:var(--len,300);opacity:0}
  32%    {opacity:.85}
  44%,76%{stroke-dashoffset:0;opacity:.85}
  100%   {stroke-dashoffset:0;opacity:0}
}

/* ---------- Footer ----------
   The last thing on every page, and the only place some of it appears: who made
   this, how to reach them, the legal documents Play requires a link to, and the
   two disclaimers that have to be visible on every page rather than only where
   they happen to be convenient — that this is not an emergency service, and who
   the map data belongs to. */
footer{
  position:relative;border-top:1px solid var(--hair);
  padding:clamp(44px,6vw,68px) 0 clamp(36px,5vw,52px);color:var(--faint);
  background:linear-gradient(180deg,rgba(10,27,74,.42),rgba(4,9,28,0) 60%);
}
/* One hairline of the spectrum along the top edge, so the page closes on the
   colour it opened with. */
footer::before{content:"";position:absolute;top:-1px;left:0;right:0;height:1px;background:var(--spectrum);opacity:.55}

.foot-top{
  display:grid;gap:clamp(32px,5vw,56px);
  grid-template-columns:1fr;
  padding-bottom:clamp(32px,4vw,44px);
}
@media (min-width:760px){.foot-top{grid-template-columns:minmax(0,1.15fr) minmax(0,1.85fr)}}

.foot-brand .brand{margin-bottom:14px}
.foot-brand p{margin:0;font-size:14.5px;line-height:1.6;color:var(--faint);max-width:34ch}

.foot-cols{
  display:grid;gap:28px 24px;
  grid-template-columns:repeat(auto-fit,minmax(min(148px,100%),1fr));
}
.foot-cols h3{
  font-family:var(--mono);font-stretch:normal;
  margin:0 0 14px;font-size:11px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--mist);font-weight:600;
}
.foot-cols ul{margin:0;padding:0;list-style:none;display:grid;gap:9px}
.foot-cols a{text-decoration:none;color:var(--faint);font-size:14.5px;transition:color .16s ease}
.foot-cols a:hover{color:var(--white)}
.foot-cols li{font-size:14.5px;line-height:1.5}
.foot-cols .muted{color:rgba(255,255,255,.3)}

.foot-bottom{
  border-top:1px solid var(--hair);padding-top:26px;
  display:grid;gap:12px;font-size:13px;line-height:1.6;
}
.foot-bottom p{margin:0;max-width:82ch}
.foot-bottom a{color:var(--mist)}

/* ---------- Scroll reveal ----------
   Armed only by the inline script in each page's <head>, which runs before
   first paint and only sets .js-reveal when it knows it can finish the job.
   Without JS, without IntersectionObserver, or with reduced motion, .rise is
   simply visible content — nothing can ever be stranded invisible.

   The reveals drive `translate` and `scale`, not `transform`, deliberately:
   `transform` is claimed by hover lifts and the staggered gallery, and an
   equal-specificity `transform:none` here would silently cancel both. */
.js-reveal .rise{
  opacity:0;translate:0 26px;
  transition:opacity .8s ease,translate .8s var(--ease-out);
}
.js-reveal .rise.in{opacity:1;translate:none}
/* A second reveal for anything that should arrive rather than slide: used on
   the phones and the signature, which read as objects, not as text. */
.js-reveal .lift{
  opacity:0;translate:0 34px;scale:.965;
  transition:opacity .9s ease,translate .9s var(--ease-out),scale .9s var(--ease-out);
}
.js-reveal .lift.in{opacity:1;translate:none;scale:1}

/* Load sequence: the hero is never scrolled to, so it plays on a timer rather
   than on an observer. .cue elements are staggered by --i, set in the page. */
.js-reveal .cue{
  opacity:0;translate:0 20px;
  animation:cue .85s var(--ease-out) forwards;
  animation-delay:calc(var(--i,0) * 110ms + 120ms);
}
@keyframes cue{to{opacity:1;translate:none}}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation:none!important;transition:none!important}
  .rise,.lift,.cue{opacity:1!important;translate:none!important;scale:1!important}
  .field::before{filter:none}
  .brand .trace{stroke-dashoffset:0}
  .wf-link{stroke-dashoffset:0}
}
