/* ==================================================================
   Chennai Rains — site.css
   Dark, cinematic, restrained. Content scrolls over a fixed WebGL sky.
   Type does the work: Marcellus to speak, IBM Plex Mono to report.
   ================================================================== */

/* ---------- fonts (self-hosted, no CDN) ---------- */
@font-face{font-family:'Marcellus';src:url('../fonts/marcellus-400.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'IBM Plex Mono';src:url('../fonts/plexmono-400.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'IBM Plex Mono';src:url('../fonts/plexmono-500.woff2') format('woff2');font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:'Inter';src:url('../fonts/inter-300.woff2') format('woff2');font-weight:300;font-style:normal;font-display:swap}
@font-face{font-family:'Inter';src:url('../fonts/inter-400.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'Inter';src:url('../fonts/inter-500.woff2') format('woff2');font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:'Great Vibes';src:url('../fonts/greatvibes-400.woff2') format('woff2');font-weight:400;font-style:normal;font-display:swap}

/* ---------- tokens ---------- */
:root{
  --void:#020308;
  --ink:#f4f6fb;
  --dim:rgba(244,246,251,.45);
  --dimmer:rgba(244,246,251,.28);
  --hair:rgba(244,246,251,.14);
  --hair-soft:rgba(244,246,251,.07);
  /* --dim is 4.2:1 on the void — right for chrome labels, short of AA
     for sentences a stranger has to trust. These two clear 4.5:1. */
  --ink-2:rgba(244,246,251,.72);
  --dim-2:rgba(244,246,251,.62);

  --rain:#7fb2ff;      /* wet accent  */
  --amber:#e0a860;     /* dry accent  */
  --steel:#8fa6c4;     /* pending     */
  --alarm:#ff8a6b;     /* heavy       */

  --accent:var(--rain);
  --plate:none;
  --plate-opacity:.16;

  --serif:'Marcellus',ui-serif,Georgia,'Times New Roman',serif;
  --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  --sans:'Inter',system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;
  --script:'Great Vibes','Snell Roundhand','Apple Chancery',cursive;

  --gut:clamp(20px,5vw,72px);
  --bar-h:58px;
  --ease:cubic-bezier(.22,.61,.36,1);
}

/* ---------- reset ---------- */
*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
html,body{margin:0;padding:0}
body{
  background:var(--void);
  color:var(--ink);
  font-family:var(--sans);
  font-weight:300;
  font-size:16px;
  line-height:1.6;
  min-height:100vh;
  overflow-x:clip;                 /* the page never scrolls sideways */
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
}
img,canvas{display:block;max-width:100%}
button,input,select{font:inherit;color:inherit}
a{color:inherit;text-decoration:none}
:focus-visible{outline:1px solid var(--accent);outline-offset:3px}
::selection{background:rgba(127,178,255,.28)}

/* thin, unobtrusive scrollbar */
*{scrollbar-width:thin;scrollbar-color:var(--hair) transparent}
*::-webkit-scrollbar{width:6px;height:6px}
*::-webkit-scrollbar-thumb{background:var(--hair);border-radius:0}
*::-webkit-scrollbar-track{background:transparent}

/* ---------- shared type ---------- */
.mono,.eyebrow,.coords,.datum__k,.field__k,.strip-block__k,.log-time,
.bar__left,.zone-cell-name,.hour-time,.day-name{
  font-family:var(--mono);
  font-size:clamp(9.5px,1.1vw,11px);
  font-weight:400;
  letter-spacing:.18em;
  text-transform:uppercase;
  color:var(--dim);
}
.eyebrow{display:block;margin:0 0 1.1rem}

/* ---------- skip link ---------- */
.skip{position:absolute;left:-9999px;top:0;z-index:100;background:var(--ink);color:var(--void);padding:.7rem 1.1rem;font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase}
.skip:focus{left:var(--gut);top:12px}

/* ================================================================
   Scene layer
   ================================================================ */
#scene,#scene-fallback,.scene-veil{
  position:fixed;inset:0;width:100%;height:100%;
  z-index:0;pointer-events:none;
}
#scene{opacity:0;transition:opacity 1.6s var(--ease)}
#scene.ready{opacity:1}

#scene-fallback{
  background:
    radial-gradient(120% 80% at 50% 8%,rgba(127,178,255,.16),transparent 62%),
    radial-gradient(90% 60% at 80% 100%,rgba(70,100,160,.14),transparent 70%),
    var(--void);
}
/* the veil keeps text legible over whatever the sky is doing */
.scene-veil{
  background:
    linear-gradient(180deg,rgba(2,3,8,.88) 0%,rgba(2,3,8,.55) 24%,rgba(2,3,8,.68) 60%,rgba(2,3,8,.94) 100%);
  z-index:1;
}

/* ================================================================
   Preloader
   ================================================================ */
#preloader{
  position:fixed;inset:0;z-index:60;background:var(--void);
  display:grid;place-items:center;
  transition:opacity .9s var(--ease),visibility .9s;
}
#preloader[hidden]{display:grid!important} /* animate out rather than snap */
#preloader.done{opacity:0;visibility:hidden;pointer-events:none}
/* DESIGN.md: the loading screen must never block past a few seconds. app.js
   caps it at 4 s, but that timer dies with the module — one 404 anywhere in
   the ES-module graph (a stale service worker after a deploy has done exactly
   that here) leaves this opaque panel over the whole site for ever, and a
   weather site nobody can read is the worst bug it has. So the panel also
   lets go on its own, late enough never to race the real hand-off. The delay
   survives prefers-reduced-motion, which only forces duration and count. */
#preloader{animation:pre-giveup 1ms linear 9s forwards}
@keyframes pre-giveup{to{opacity:0;visibility:hidden;pointer-events:none}}
.pre__inner{width:min(78vw,320px);text-align:center}
.pre__mark{font-family:var(--script);font-size:clamp(38px,9vw,62px);line-height:1;color:var(--ink);margin-bottom:1.6rem;opacity:.96}
.pre__rule{height:1px;width:100%;background:var(--hair);position:relative;overflow:hidden}
.pre__fill{position:absolute;inset:0 auto 0 0;width:0;background:var(--ink);transition:width .5s var(--ease)}
.pre__line{margin-top:1.1rem;display:block}
#preloader-pct{font-family:var(--mono);font-size:11px;letter-spacing:.22em;color:var(--dim);display:block;margin-top:.9rem}

/* ================================================================
   Shell + masthead
   ================================================================ */
.shell{position:relative;z-index:2;padding-bottom:calc(var(--bar-h) + 3rem)}

.masthead{
  position:relative;z-index:12;
  display:flex;align-items:flex-start;justify-content:space-between;gap:1.5rem;
  padding:clamp(20px,4vw,44px) var(--gut) 0;
}
.logotype{
  font-family:var(--script);
  font-size:clamp(34px,6.4vw,64px);
  line-height:.96;font-weight:400;margin:0;
  letter-spacing:.01em;
}
.coords{display:block;margin-top:.85rem;color:var(--dim)}
/* The greeting speaks in the readout's voice. Empty means gone — no stray
   line, no separator left hanging under the logotype. */
.greeting{margin:.55rem 0 0;color:var(--dim-2)}
#greeting:empty{display:none}

.menu-btn{
  flex:none;width:46px;height:46px;
  background:transparent;border:1px solid var(--hair);
  display:grid;place-content:center;gap:5px;cursor:pointer;
  transition:border-color .3s var(--ease),background .3s var(--ease);
}
.menu-btn:hover{border-color:var(--dim);background:rgba(244,246,251,.03)}
.menu-btn__bars{display:block;width:18px;height:1px;background:var(--ink)}
.menu-btn__bars::before,.menu-btn__bars::after{content:'';display:block;width:18px;height:1px;background:var(--ink);position:relative}
.menu-btn__bars::before{top:-6px}
.menu-btn__bars::after{top:5px}

#menu-panel{
  position:absolute;right:var(--gut);top:calc(100% + 10px);z-index:20;
  min-width:230px;background:rgba(4,6,14,.94);
  border:1px solid var(--hair);padding:1.4rem 1.5rem;
  backdrop-filter:blur(14px);-webkit-backdrop-filter:blur(14px);
}
#menu-panel[hidden]{display:none}
.menu__list{list-style:none;margin:0 0 1.2rem;padding:0;display:grid;gap:.85rem}
.menu__list a{font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--dim);transition:color .25s}
.menu__list a:hover{color:var(--ink)}
.menu__foot{border-top:1px solid var(--hair-soft);padding-top:1rem}
.menu__no{color:var(--dimmer)}

/* ================================================================
   Hero + verdict
   ================================================================ */
.hero{
  min-height:min(88vh,760px);
  display:flex;flex-direction:column;justify-content:center;
  padding:clamp(48px,12vh,120px) var(--gut) clamp(40px,8vh,80px);
}

.verdict{position:relative;isolation:isolate;overflow:hidden;padding:clamp(24px,4vw,48px) 0}
/* the cinematic plate, tinted per state, sitting under the words */
.verdict__plate{
  position:absolute;inset:-8% -6%;z-index:-1;
  background-image:var(--plate);
  background-size:cover;background-position:center;
  opacity:var(--plate-opacity);
  filter:saturate(.85) contrast(1.05);
  -webkit-mask-image:radial-gradient(72% 68% at 42% 46%,#000 0%,transparent 78%);
  mask-image:radial-gradient(72% 68% at 42% 46%,#000 0%,transparent 78%);
  transition:opacity 1.2s var(--ease),background-image .6s var(--ease);
  pointer-events:none;
}
.verdict__headline{
  font-family:var(--serif);font-weight:400;
  font-size:clamp(34px,7.2vw,86px);
  line-height:1.04;letter-spacing:-.012em;
  margin:0 0 1.5rem;max-width:19ch;
  text-wrap:balance;
}
.verdict__detail{
  font-size:clamp(15px,1.9vw,21px);font-weight:300;
  line-height:1.62;color:rgba(244,246,251,.76);
  margin:0 0 2rem;max-width:56ch;
}
.verdict__quip{
  font-family:var(--serif);font-style:italic;
  font-size:clamp(16px,2.1vw,24px);line-height:1.5;
  color:var(--accent);margin:0;max-width:46ch;
  padding-left:1.15rem;border-left:1px solid var(--accent);
  transition:color .6s var(--ease),border-color .6s var(--ease);
}
.verdict__quip[hidden]{display:none}

/* ---- provenance: which source is speaking ----
   Radar observes, the model predicts, and the page has to say which one it
   is quoting. The mono key carries the label treatment; the sentence beside
   it stays sentence-case, because a stranger has to read it, not decode it.
   Empty means gone — no orphan rule, no stray gap under the detail. */
.verdict__source{
  margin:0 0 1.9rem;max-width:56ch;display:block;
  padding-left:1.15rem;border-left:1px solid var(--hair);
}
#verdict-source:empty,#verdict-source[hidden]{display:none}
/* Label above sentence, always — the site's own eyebrow idiom. Inline would
   put the key beside a short line and above a long one, which reads as a
   layout accident rather than a decision. */
.vsrc__k{
  display:block;margin-bottom:.45rem;
  font-family:var(--mono);font-size:clamp(9.5px,1.1vw,11px);
  font-weight:500;letter-spacing:.18em;text-transform:uppercase;
  color:var(--dim-2);
}
.vsrc__t{
  display:block;
  font-size:clamp(13.5px,1.5vw,15.5px);line-height:1.6;
  color:var(--ink-2);
}
/* Radar is the observation. It gets the rain blue and a solid rule; the
   model keeps the hairline it always had. No new accent colour either way. */
.verdict__source[data-source="radar"]{border-left-color:var(--rain)}
.verdict__source[data-source="radar"] .vsrc__k{color:var(--rain)}
.verdict__source[data-source="radar"] .vsrc__t{color:var(--ink)}
/* An overdue sweep is still radar talking, so it keeps the key — but not the
   rain blue, which on this page means "this is happening". It borrows the
   same amber the age beside it uses for "check this before relying on it". */
.verdict__source[data-source="stale"]{border-left-color:var(--amber)}
.verdict__source[data-source="stale"] .vsrc__k{color:var(--amber)}
.verdict__source[data-source="stale"] .vsrc__t{color:var(--ink)}

/* ---- the mono footer row: confidence, and the radar frame's age ---- */
.verdict__meta{
  margin:1.8rem 0 0;display:flex;align-items:baseline;
  gap:.5rem clamp(14px,2.4vw,28px);flex-wrap:wrap;
}
.verdict__confidence{
  font-family:var(--mono);font-size:11px;letter-spacing:.18em;
  color:var(--dim);margin:0;
}
.verdict__confidence[data-empty="true"]{color:var(--dimmer)}
/* Stale radar must not look live. Fresh reads as a quiet fact; overdue
   borrows the site's existing "check this before relying on it" amber.
   --dim-2 rather than --dim: --dim is 4.2:1, fine for a standing chrome
   label, short of AA for the one number that tells you whether the
   observation you are being shown is still true. */
.verdict__age{
  font-family:var(--mono);font-size:11px;letter-spacing:.18em;
  color:var(--dim-2);margin:0;
}
#radar-age:empty,#radar-age[hidden]{display:none}
.verdict__age[data-stale="stale"]{color:var(--amber)}
.verdict__age[data-stale="dead"]{color:var(--amber);text-decoration:line-through;text-decoration-thickness:1px}

/* per-state accents + plates */
.verdict[data-state="DRY_DAY"],.verdict[data-state="TEASED"]{--accent:var(--amber);--plate:url('../media/plate-dry.jpg')}
.verdict[data-state="TEASED"]{--plate-opacity:.13}
.verdict[data-state="NEAR_MISS"],.verdict[data-state="RAIN_SOON"]{--accent:var(--steel);--plate:url('../media/plate-soon.jpg')}
.verdict[data-state="DRIZZLE"]{--accent:#a9c4e4;--plate:url('../media/plate-drizzle.jpg')}
.verdict[data-state="RAINING"]{--accent:var(--rain);--plate:url('../media/plate-storm.jpg');--plate-opacity:.2}
.verdict[data-state="HEAVY"]{--accent:var(--alarm);--plate:url('../media/plate-storm.jpg');--plate-opacity:.28}

.hero__scroll{margin-top:auto;padding-top:clamp(28px,6vh,56px);color:var(--dimmer)}

/* ================================================================
   Sections
   ================================================================ */
.section{padding:clamp(56px,11vh,124px) var(--gut);position:relative}
.section + .section{border-top:1px solid var(--hair-soft)}
.section__head{margin-bottom:clamp(28px,5vh,52px);max-width:64ch}
.section__title{
  font-family:var(--serif);font-weight:400;
  font-size:clamp(24px,3.6vw,42px);line-height:1.16;
  margin:0 0 .8rem;letter-spacing:-.008em;
}
.section__lede{margin:0;color:var(--dim);font-size:clamp(14px,1.6vw,17px);max-width:58ch;line-height:1.65}

/* reveal-on-scroll */
.reveal{opacity:0;transform:translateY(22px);transition:opacity 1s var(--ease),transform 1s var(--ease)}
.reveal.in{opacity:1;transform:none}

/* ================================================================
   Now
   ================================================================ */
.now__lead{display:flex;align-items:flex-start;gap:clamp(14px,2.4vw,30px);flex-wrap:wrap}
.now__temp{font-family:var(--serif);font-size:clamp(64px,15vw,164px);line-height:.86;letter-spacing:-.03em}
.now__meta{padding-top:.7rem;display:grid;gap:.4rem}
.now__label{font-size:clamp(15px,2vw,22px);color:rgba(244,246,251,.82)}
.now__feels{color:var(--dim);font-size:14px}
.now__grid{
  margin-top:clamp(30px,5vh,54px);
  display:grid;gap:1px;background:var(--hair-soft);
  grid-template-columns:repeat(auto-fit,minmax(140px,1fr));
  border:1px solid var(--hair-soft);
}
.datum{background:var(--void);padding:1.15rem 1.25rem;display:grid;gap:.5rem}
.datum__v{font-family:var(--serif);font-size:clamp(19px,2.6vw,28px);line-height:1}

/* ================================================================
   Strips (nowcast + hourly)
   ================================================================ */
.strip-block{margin-bottom:clamp(28px,5vh,48px)}
.strip-block__head{display:flex;align-items:baseline;justify-content:space-between;gap:1rem;margin-bottom:1.1rem;flex-wrap:wrap}
.strip-block__hint{font-size:13px;color:var(--dimmer)}
.strip{overflow-x:auto;overflow-y:hidden;padding-bottom:.5rem;-webkit-overflow-scrolling:touch}
.strip::-webkit-scrollbar{height:2px}
/* #nowcast stacks (bars over axis); #hourly is a single scrolling row of cards.
   Both carry .strip, so the row behaviour is scoped to the hour cards only. */
#hourly{display:flex;align-items:flex-end;gap:clamp(6px,1vw,12px);min-height:0}

.strip-bars{display:flex;align-items:flex-end;gap:2px;height:clamp(84px,15vh,132px);min-width:max-content}
.strip-bar{
  position:relative;flex:0 0 auto;width:clamp(7px,1.1vw,12px);
  background:linear-gradient(180deg,var(--rain),rgba(127,178,255,.22));
  opacity:calc(.28 + var(--v,0) * .72);
  transition:height .7s var(--ease),opacity .7s var(--ease);
}
.strip-bar[data-dry="true"]{background:linear-gradient(180deg,rgba(244,246,251,.22),rgba(244,246,251,.05))}
.strip-axis{
  display:flex;justify-content:space-between;align-items:baseline;
  width:100%;min-width:max-content;gap:1.2rem;margin-top:.6rem;
}
.strip-tick{flex:0 0 auto;font-family:var(--mono);font-size:9.5px;letter-spacing:.12em;color:var(--dimmer);white-space:nowrap}

.hour{flex:0 0 auto;display:grid;gap:.5rem;justify-items:center;min-width:clamp(52px,7vw,72px)}
.hour-bar{width:100%;height:clamp(52px,9vh,84px);display:flex;align-items:flex-end;background:var(--hair-soft)}
.hour-fill{width:100%;background:linear-gradient(180deg,var(--rain),rgba(127,178,255,.28));transition:height .7s var(--ease)}
.hour-temp{font-family:var(--serif);font-size:16px}
.hour-prob{font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;color:var(--dimmer)}

/* ================================================================
   Daily
   ================================================================ */
.daily{display:grid;gap:1px;background:var(--hair-soft);border:1px solid var(--hair-soft)}
.day{
  background:var(--void);display:grid;align-items:center;gap:.8rem 1.2rem;
  grid-template-columns:minmax(78px,1fr) minmax(88px,1.4fr) minmax(120px,2fr);
  padding:.85rem 1.2rem;min-height:0;
}
.day[data-today="true"]{background:rgba(127,178,255,.05)}
.day-date{display:grid;gap:.15rem}
.day-label{font-size:13px;color:var(--dim)}
.day-precip{display:grid;gap:.25rem}
.day-prob{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--dimmer)}
.day-bar{height:2px;background:var(--hair-soft);position:relative;overflow:hidden}
.day-fill{position:absolute;inset:0 auto 0 0;background:var(--rain);transition:width .7s var(--ease)}
.day-range{display:flex;align-items:baseline;gap:.7rem;justify-content:flex-end}
.day-max{font-family:var(--serif);font-size:19px}
.day-min{color:var(--dimmer);font-size:14px}

/* ================================================================
   Zone grid — the 20 km ring
   ================================================================ */
.zone-grid{display:grid;gap:clamp(24px,4vh,44px)}
.zone{display:grid;gap:1rem}
.zone-cells{display:grid;gap:1px;background:var(--hair-soft);grid-template-columns:repeat(auto-fill,minmax(150px,1fr));border:1px solid var(--hair-soft)}
.zone-cell{
  background:var(--void);padding:1rem 1.05rem;display:grid;gap:.55rem;
  transition:background .3s var(--ease);
}
.zone-cell:hover{background:rgba(244,246,251,.03)}
.zone-cell[data-selected="true"]{background:rgba(127,178,255,.08);box-shadow:inset 2px 0 0 var(--rain)}
.zone-cell-temp{font-family:var(--serif);font-size:22px;line-height:1}
.zone-cell-mm{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--dim)}
.zone-cell-note{font-size:12px;color:var(--dimmer);line-height:1.45}
.zone-cell[data-state="RAINING"] .zone-cell-mm,
.zone-cell[data-state="HEAVY"] .zone-cell-mm{color:var(--rain)}
.zone-cell[data-state="HEAVY"]{box-shadow:inset 2px 0 0 var(--alarm)}

/* ---- radar-observed localities ----
   The payoff for drawing a 20 km ring at all. The forecast model reads one
   ~11 km cell for a dozen of these names, so it cannot tell them apart even
   in principle; radar can, at roughly 0.3 km a pixel. When it sees rain over
   one of them, that cell stops being a row in a table.

   The whole cell lights in the rain blue already in the palette — no new
   accent — and is ringed rather than stripe-marked, so it reads at a glance
   across a grid of thirty-nine without shouting. */
.zone-cell[data-observed="true"]{
  /* .62 not .5: the outline is a UI component carrying meaning, so it has
     to clear 3:1 against the void on its own (WCAG 1.4.11). Held in a
     variable so the stripe rules below can compose with the ring instead of
     replacing it — box-shadow is one property, and a cell that is both
     observed and HEAVY has to be able to show both marks. */
  --obs-ring:rgba(127,178,255,.62);
  background:rgba(127,178,255,.09);
  box-shadow:inset 0 0 0 1px var(--obs-ring);
}
.zone-cell[data-observed="true"] .zone-cell-name{color:var(--ink)}
/* The model's own number stays visible next to the badge. When it reads
   0.0 mm under a RADAR · RAIN line, that contradiction is the news — but
   where the model agrees it is raining, its number keeps the rain blue it
   has everywhere else on the page. */
.zone-cell[data-observed="true"]:not([data-state="RAINING"]):not([data-state="HEAVY"]) .zone-cell-mm{
  color:var(--dim-2);
}
/* Coarse extent only — three steps, from a patch of echo to a solid cell.
   Nothing here claims a rain rate; no colour→dBZ table is assumed. */
.zone-cell[data-observed="true"][data-obs-level="2"]{
  --obs-ring:rgba(127,178,255,.8);background:rgba(127,178,255,.13);
}
.zone-cell[data-observed="true"][data-obs-level="3"]{
  --obs-ring:var(--rain);background:rgba(127,178,255,.18);
}
.zone-cell[data-observed="true"]:hover{background:rgba(127,178,255,.22)}
/* Radar adds a mark, it never takes one away: the selected cell keeps its
   rain stripe and a HEAVY cell keeps the alarm stripe it has always had. */
.zone-cell[data-observed="true"][data-selected="true"]{
  box-shadow:inset 0 0 0 1px var(--obs-ring),inset 3px 0 0 var(--rain);
}
.zone-cell[data-observed="true"][data-state="HEAVY"]{
  box-shadow:inset 0 0 0 1px var(--obs-ring),inset 2px 0 0 var(--alarm);
}
.zone-cell[data-observed="true"][data-state="HEAVY"][data-selected="true"]{
  box-shadow:inset 0 0 0 1px var(--obs-ring),inset 3px 0 0 var(--alarm);
}
.zone-cell-obs{
  display:flex;align-items:center;gap:.45rem;
  font-family:var(--mono);font-size:9.5px;font-weight:500;
  letter-spacing:.16em;text-transform:uppercase;
  color:var(--rain);line-height:1.2;
}
.zone-cell-obs::before{
  content:'';flex:none;width:5px;height:5px;border-radius:50%;
  background:var(--rain);
  animation:pulse 2.6s var(--ease) infinite;
}
@media (prefers-reduced-motion:reduce){
  .zone-cell-obs::before{animation:none}
}

/* ================================================================
   Controls
   ================================================================ */
.panel{display:grid;gap:1px;background:var(--hair-soft);border:1px solid var(--hair-soft);max-width:720px}
.field{background:var(--void);padding:1.25rem 1.3rem;display:grid;gap:.75rem}
.field__note{font-size:13px;color:var(--dim-2);line-height:1.55;margin:0}
/* Amber is already this site's "read this before you rely on it" (#offline-banner).
   9.7:1 on the void, and the sentence carries the warning on its own. */
.field__note--warn{color:var(--amber)}
.field--switch{grid-template-columns:minmax(0,1fr);gap:.9rem}
.field--actions{grid-auto-flow:row}
/* Interval and quiet hours are one decision; they share a row until the
   panel is too narrow to hold both. */
.field--pair{grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:1.15rem 1.6rem;align-items:start}
.subfield{display:grid;gap:.7rem;align-content:start;min-width:0}
.field--pair > .field__note{grid-column:1 / -1}

.select{
  position:relative;display:block;
}
.select select{
  width:100%;appearance:none;-webkit-appearance:none;
  background:transparent;border:0;border-bottom:1px solid var(--hair);
  padding:.55rem 2rem .55rem 0;
  font-family:var(--serif);font-size:clamp(17px,2.2vw,22px);
  cursor:pointer;border-radius:0;
  transition:border-color .3s var(--ease);
}
.select select:hover{border-bottom-color:var(--dim)}
.select select option,.select select optgroup{background:#080b16;color:var(--ink);font-family:var(--sans)}
.select__caret{position:absolute;right:.3rem;top:50%;transform:translateY(-50%);pointer-events:none;color:var(--dim);font-size:11px}
.select--narrow select{font-size:16px}

.quiet{display:flex;align-items:center;gap:.8rem;flex-wrap:wrap}
.quiet input[type="time"]{
  background:transparent;border:0;border-bottom:1px solid var(--hair);
  padding:.45rem .2rem;font-family:var(--mono);font-size:13px;letter-spacing:.08em;
  color-scheme:dark;border-radius:0;
}
.quiet input[type="time"]:hover{border-bottom-color:var(--dim)}
.quiet__dash{color:var(--dimmer)}
.quiet__leg{margin:0}

/* switch */
.switch{position:relative;display:inline-flex;align-items:center;cursor:pointer}
.switch__input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
.switch__track{
  width:46px;height:24px;border:1px solid var(--hair);
  display:block;position:relative;transition:border-color .3s var(--ease),background .3s var(--ease);
}
.switch__dot{
  position:absolute;top:3px;left:3px;width:16px;height:16px;
  background:var(--dim);transition:transform .3s var(--ease),background .3s var(--ease);
}
.switch__input:checked + .switch__track{border-color:var(--rain);background:rgba(127,178,255,.12)}
.switch__input:checked + .switch__track .switch__dot{transform:translateX(22px);background:var(--rain)}
.switch__input:focus-visible + .switch__track{outline:1px solid var(--accent);outline-offset:3px}
.switch__label{margin-left:.85rem}

/* buttons */
.btnrow{display:flex;gap:.7rem;flex-wrap:wrap}
.btn{
  background:transparent;border:1px solid var(--hair);color:var(--ink);
  font-family:var(--mono);font-size:11px;letter-spacing:.18em;text-transform:uppercase;
  padding:.8rem 1.3rem;cursor:pointer;border-radius:0;
  transition:border-color .3s var(--ease),background .3s var(--ease),color .3s var(--ease);
}
.btn:hover:not(:disabled){border-color:var(--ink);background:rgba(244,246,251,.05)}
.btn:disabled{opacity:.42;cursor:not-allowed}
.btn--ghost{border-color:var(--hair-soft);color:var(--dim)}
.btn--ghost:hover:not(:disabled){color:var(--ink)}
/* One step up from the outline, no further: the border simply fills. */
.btn--primary{border-color:var(--ink);color:var(--ink)}
.btn--primary:hover:not(:disabled){background:var(--ink);color:var(--void);border-color:var(--ink)}

#geo-status,#coverage-note{font-size:13px;color:var(--dim-2);line-height:1.55;margin:0}

/* ================================================================
   Alerts — one status, then the reasons
   ================================================================ */

/* The two delivery facts no control on screen can show you. */
.notif-facts{color:var(--ink-2)}

/* Delivery. The stripe reuses the state idiom already in this file
   (.log-row, .zone-cell): a 2px inset rule, never a badge. */
.tier{--tier:var(--hair);--tier-ink:var(--ink-2);box-shadow:inset 2px 0 0 var(--tier);gap:1.05rem}
.tier[data-tier="checking"]{--tier:var(--steel);--tier-ink:var(--steel)}
.tier[data-tier="tab"]{--tier:var(--amber);--tier-ink:var(--amber)}
.tier[data-tier="push"]{--tier:var(--rain);--tier-ink:var(--rain)}
.tier[data-tier="blocked"]{--tier:var(--alarm);--tier-ink:var(--alarm)}

/* The one line that answers "is this working?" — read before anything else
   in the panel. The mark carries the state in colour, the words carry it
   on their own, and neither depends on the other. */
.tier__lead{margin:0;display:flex;align-items:center;gap:.75rem}
.tier__mark{flex:none;width:9px;height:9px;background:var(--tier);border:1px solid var(--tier-ink)}
.tier__tag{
  margin:0;font-family:var(--mono);
  font-size:clamp(12px,1.5vw,14.5px);letter-spacing:.2em;
  text-transform:uppercase;line-height:1.2;color:var(--tier-ink);
}
#notif-status{margin:0;font-size:14px;line-height:1.6;color:var(--ink-2)}

.tier__act{margin:0;display:flex;align-items:center;gap:.6rem 1rem;flex-wrap:wrap}
.tier__hint{font-size:12.5px;line-height:1.5;color:var(--dim-2)}

/* The next step, when there is one. It borrows the block's own state colour
   rather than inventing a fifth accent, and it is simply not on the page when
   app.js has nothing to ask of anyone. */
.tier__next{
  margin:0;font-size:13.5px;line-height:1.6;color:var(--ink);
  padding-left:.85rem;border-left:1px solid var(--tier);
}
#notif-next[hidden],#notif-next:empty{display:none}

/* The reasons, four across a strip instead of four stacked rows. Labels are
   short enough not to wrap; the sentence sits under its own label. */
.checks{
  list-style:none;margin:0;padding:0;
  display:grid;gap:1px;background:var(--hair-soft);border:1px solid var(--hair-soft);
  /* Fixed counts, not auto-fit: auto-fit lands on three-plus-an-orphan
     through the tablet band, and a lone cell reads as a mistake. */
  grid-template-columns:repeat(4,minmax(0,1fr));
}
@media (max-width:900px){.checks{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:460px){.checks{grid-template-columns:minmax(0,1fr)}}
.check{
  background:var(--void);padding:.8rem .9rem;
  display:grid;grid-template-columns:7px minmax(0,1fr);
  gap:.45rem .6rem;align-items:baseline;align-content:start;
}
.check__mark{width:7px;height:7px;align-self:center;border:1px solid var(--dimmer);background:transparent}
.check__k{color:var(--dim-2);white-space:nowrap}
.check__v{grid-column:1 / -1;font-size:12.5px;line-height:1.5;color:var(--ink-2);overflow-wrap:anywhere}
.check[data-state="checking"] .check__mark{border-color:var(--steel);animation:pulse 1.8s var(--ease) infinite}
.check[data-state="ok"] .check__mark{background:var(--rain);border-color:var(--rain)}
.check[data-state="warn"] .check__mark{background:var(--amber);border-color:var(--amber)}
.check[data-state="fail"] .check__mark{background:var(--alarm);border-color:var(--alarm)}
.check[data-state="ok"] .check__v,
.check[data-state="warn"] .check__v,
.check[data-state="fail"] .check__v{color:var(--ink)}

.logblock__note{margin:.5rem 0 1.1rem;color:var(--dim-2)}

/* The pulsing mark is the only motion here; the global reduce block would
   freeze it mid-fade, so stop it outright instead. */
@media (prefers-reduced-motion:reduce){
  .check[data-state="checking"] .check__mark{animation:none}
}

/* ================================================================
   Alert log
   ================================================================ */
.logblock{max-width:720px}
.log{list-style:none;margin:0;padding:0;display:grid;gap:1px;background:var(--hair-soft);border:1px solid var(--hair-soft)}
.log-row{background:var(--void);padding:1rem 1.15rem;display:grid;gap:.4rem}
.log-row[data-state="HEAVY"]{box-shadow:inset 2px 0 0 var(--alarm)}
.log-row[data-state="RAINING"],.log-row[data-state="RAIN_SOON"]{box-shadow:inset 2px 0 0 var(--rain)}
.log-row[data-state="DRY_DAY"],.log-row[data-state="TEASED"]{box-shadow:inset 2px 0 0 var(--amber)}
.log-title{font-family:var(--serif);font-size:17px;line-height:1.35}
.log-body{font-size:13px;color:var(--dim);line-height:1.55}
.log-place{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--dimmer);text-transform:uppercase}
.log-empty{background:var(--void);padding:1.4rem 1.15rem;color:var(--dimmer);font-size:14px}
.empty-note{color:var(--dimmer);font-size:13px;padding:.9rem 0}

/* ================================================================
   Bottom bar
   ================================================================ */
#bottombar{
  position:fixed;left:0;right:0;bottom:0;z-index:40;height:var(--bar-h);
  display:flex;align-items:center;gap:clamp(12px,2.4vw,26px);
  padding:0 var(--gut) 0 var(--gut);
  padding-bottom:env(safe-area-inset-bottom);
  background:rgba(3,4,11,.92);border-top:1px solid var(--hair);
  backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
}
.bar__left{display:flex;align-items:center;gap:.7rem;flex:none;color:var(--ink)}
.bar__dot{width:6px;height:6px;background:var(--accent);border-radius:50%;flex:none;animation:pulse 2.6s var(--ease) infinite}
.bar__quip{
  flex:1;min-width:0;font-size:13.5px;color:var(--dim);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
.bar__meter{display:flex;align-items:flex-end;gap:3px;height:18px;flex:none}
.meter-bar{
  width:3px;background:var(--accent);display:block;
  height:calc(5px + var(--i,0) * 3px);
  transition:opacity .5s var(--ease),background .5s var(--ease);
}
#bottombar[data-state] .meter-bar.on{animation:meter 1.1s var(--ease) infinite alternate;animation-delay:calc(var(--i,0) * .1s)}
#bottombar[data-state="DRY_DAY"],#bottombar[data-state="TEASED"]{--accent:var(--amber)}
#bottombar[data-state="NEAR_MISS"],#bottombar[data-state="RAIN_SOON"]{--accent:var(--steel)}
#bottombar[data-state="HEAVY"]{--accent:var(--alarm)}
@keyframes meter{from{transform:scaleY(.55)}to{transform:scaleY(1)}}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}

/* ================================================================
   Banner, colophon, noscript
   ================================================================ */
#offline-banner{
  position:fixed;left:50%;transform:translateX(-50%);
  bottom:calc(var(--bar-h) + 16px);z-index:45;
  background:rgba(8,10,20,.96);border:1px solid var(--hair);
  padding:.75rem 1.2rem;font-family:var(--mono);font-size:10.5px;
  letter-spacing:.16em;text-transform:uppercase;color:var(--amber);
  max-width:calc(100vw - 2 * var(--gut));text-align:center;
}
#offline-banner[hidden]{display:none}

.colophon{padding:clamp(48px,9vh,96px) var(--gut) clamp(30px,5vh,52px);border-top:1px solid var(--hair-soft)}
.colophon__cols{display:grid;gap:1.6rem;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));margin-bottom:2rem}
.colophon__mark{font-family:var(--script);font-size:30px;line-height:1}
.colophon__note{font-size:12.5px;color:var(--dimmer);line-height:1.7;margin:0}

.noscript{padding:1.2rem var(--gut);background:rgba(224,168,96,.1);border-bottom:1px solid var(--hair);font-size:14px}

/* ================================================================
   Skeleton
   ================================================================ */
.skeleton,[data-loading="true"] .now__temp,[data-loading="true"] .verdict__headline{
  background:linear-gradient(90deg,var(--hair-soft) 0%,rgba(244,246,251,.12) 50%,var(--hair-soft) 100%);
  background-size:200% 100%;animation:sheen 1.7s linear infinite;
  color:transparent!important;border-radius:2px;
}
@keyframes sheen{from{background-position:200% 0}to{background-position:-200% 0}}

/* ================================================================
   Responsive
   ================================================================ */
@media (max-width:900px){
  .day{grid-template-columns:minmax(70px,1fr) minmax(80px,1.2fr);row-gap:.45rem;padding:.7rem 1rem}
  .day-range{grid-column:1 / -1;justify-content:flex-start;order:3}
  .day-max{font-size:17px}
}
@media (max-width:620px){
  :root{--bar-h:52px}
  .masthead{padding-top:20px}
  .now__lead{gap:1rem}
  .now__meta{padding-top:0}
  .field--switch{grid-template-columns:1fr;gap:1rem}
  .bar__quip{display:none}          /* the bar keeps state + meter on tiny screens */
  #menu-panel{left:var(--gut);right:var(--gut);min-width:0}
}
@media (min-width:1600px){
  .shell{max-width:1560px;margin:0 auto}
}

/* ================================================================
   Reduced motion — kill every animation, keep the site whole
   ================================================================ */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.001ms!important;animation-iteration-count:1!important;
    transition-duration:.001ms!important;scroll-behavior:auto!important;
  }
  .reveal{opacity:1;transform:none}
  #scene{opacity:1}
  .bar__dot{animation:none}
}

/* ================================================================
   Ambient rain + droplet cursor
   ================================================================ */

/* Sits above the globe, below the content, so text stays legible. */
.rain-layer{
  position:fixed;inset:0;z-index:1;
  pointer-events:none;
  mix-blend-mode:screen;
}

/* --- droplet cursor --- */
.droplet-cursor{
  position:fixed;top:0;left:0;z-index:9999;
  width:22px;height:29px;margin:-6px 0 0 -11px;   /* tip sits on the hotspot */
  pointer-events:none;
  will-change:transform;
  transition:opacity .25s var(--ease);
}
.droplet-cursor svg{
  display:block;overflow:visible;
  filter:drop-shadow(0 2px 6px rgba(2,3,8,.75));
  transition:transform .22s var(--ease);
}
.droplet-cursor path{
  fill:rgba(150,195,255,.34);
  stroke:rgba(190,220,255,.9);
  stroke-width:1.1;
}
.droplet-glint{fill:rgba(244,246,251,.5)}

/* over anything clickable the drop swells and brightens */
.droplet-cursor.is-hot svg{transform:scale(1.3)}
.droplet-cursor.is-hot path{fill:rgba(190,220,255,.5);stroke:rgba(224,240,255,1)}

/* Hide the native cursor only once ours is actually on the page, and never
   over text fields — a droplet is a poor caret. */
.has-droplet-cursor,
.has-droplet-cursor a,
.has-droplet-cursor button,
.has-droplet-cursor select,
.has-droplet-cursor summary,
.has-droplet-cursor label{cursor:none}
.has-droplet-cursor input[type="time"],
.has-droplet-cursor input[type="text"],
.has-droplet-cursor textarea{cursor:auto}

/* Touch devices have no cursor to replace; reduced motion gets the real one. */
@media (pointer:coarse),(hover:none){
  .droplet-cursor{display:none}
  .has-droplet-cursor,.has-droplet-cursor *{cursor:auto}
}
@media (prefers-reduced-motion:reduce){
  .droplet-cursor{display:none}
  .has-droplet-cursor,.has-droplet-cursor *{cursor:auto}
  .rain-layer{opacity:.5}
}

/* ================================================================
   Account (optional, hidden unless the server enables it)
   ================================================================ */
.field__opt{color:var(--dimmer);letter-spacing:.18em}
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}
.authrow{display:flex;gap:.7rem;flex-wrap:wrap;align-items:stretch}
.authinput{
  flex:1 1 12rem;min-width:0;
  background:transparent;border:0;border-bottom:1px solid var(--hair);
  padding:.7rem .2rem;font-family:var(--sans);font-size:15px;
  color:var(--ink);border-radius:0;
  transition:border-color .3s var(--ease);
}
.authinput::placeholder{color:var(--dimmer)}
.authinput:hover,.authinput:focus{border-bottom-color:var(--dim);outline:none}
/* :focus{outline:none} above is for the mouse; keyboard focus keeps the
   site's ring, so the account field is no harder to see than any button. */
.authinput:focus-visible{border-bottom-color:var(--accent);outline:1px solid var(--accent);outline-offset:3px}
.auth-who{color:var(--ink);font-family:var(--mono);font-size:12px;letter-spacing:.1em}
#auth-status:empty{display:none}
#auth-status{color:var(--accent)}
#auth-status[data-ok="false"]{color:var(--amber)}

/* Supabase's own mailer is rate limited and slow; a countdown reads as a
   queue, a dead button reads as a bug. Keep the label legible while it waits. */
/* The block gained two more lines; give it the panel's own vertical rhythm.
   [hidden] has to win here — app.js toggles these two by that property. */
#account-out,#account-in{display:grid;gap:.75rem}
#account-out[hidden],#account-in[hidden]{display:none}
.auth-cool{margin:0;color:var(--dim-2);letter-spacing:.16em}
.auth-cool[hidden]{display:none}
.auth-fine{color:var(--dim-2)}
.btn[data-cooling="true"]:disabled{opacity:1;border-color:var(--hair-soft);color:var(--dim-2);cursor:progress}

/* ================================================================
   Opening identity screen — #welcome

   A first frame, not a gate. It is shipped with [hidden] and a controller
   lifts it after the preloader has gone; skipping it, or never showing it
   at all, leaves the forecast exactly where it was. Kept below the
   preloader's z-index so the two can never argue over the same second.
   ================================================================ */
#welcome{
  position:fixed;inset:0;z-index:55;
  display:flex;
  overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
  padding:clamp(28px,7vh,72px) var(--gut);
  /* Opaque, like the preloader. At .97 the hero headline behind ghosted
     straight through the frame — "Your name" landed on top of "No rain
     expected" — and 3 % of #f4f6fb over the void is plainly legible on a
     dark screen. The atmosphere is the two gradients, not the leak. */
  background:
    radial-gradient(120% 80% at 50% 0%,rgba(127,178,255,.13),transparent 60%),
    radial-gradient(90% 60% at 85% 100%,rgba(70,100,160,.12),transparent 72%),
    var(--void);
  animation:welcome-in .9s var(--ease) both;
}
/* An id sets display here, so the UA's own [hidden] rule loses on
   specificity. Say it outright: hidden means gone — no clicks caught, no
   focus stop, no reader detour. */
#welcome[hidden]{display:none!important}

/* margin:auto on a flex child centres without clipping the top when the
   frame is taller than a short phone in landscape. */
.welcome__frame{margin:auto;width:min(100%,540px)}
.welcome__mark{
  font-family:var(--script);font-size:clamp(34px,7.4vw,58px);
  line-height:1;color:var(--ink);margin:0 0 clamp(20px,4vh,34px);
}
.welcome__title{
  font-family:var(--serif);font-weight:400;
  font-size:clamp(27px,5vw,46px);line-height:1.1;letter-spacing:-.01em;
  margin:0 0 .9rem;max-width:15ch;text-wrap:balance;
}
.welcome__sub{
  margin:0 0 clamp(20px,3.4vh,30px);
  font-size:15px;line-height:1.6;color:var(--ink-2);max-width:44ch;
}

.welcome__ask{display:flex;align-items:stretch;gap:.75rem;flex-wrap:wrap}
.welcome__input{
  flex:1 1 13rem;min-width:0;
  background:transparent;border:0;border-bottom:1px solid var(--hair);
  padding:.6rem .1rem;border-radius:0;color:var(--ink);
  font-family:var(--serif);font-size:clamp(19px,2.4vw,23px);
  transition:border-color .3s var(--ease);
}
/* --dimmer is 2.2:1 — fine for chrome, not for the only visible name a
   field has. .5 keeps it obviously unfilled and still clears AA (5.0:1). */
.welcome__input::placeholder{color:rgba(244,246,251,.5);opacity:1}
.welcome__input:hover{border-bottom-color:var(--dim)}
.welcome__input:focus{outline:none;border-bottom-color:var(--ink)}
/* 16px keeps iOS from zooming the page on focus. */
.welcome__input--mail{font-family:var(--sans);font-size:16px;padding:.75rem .1rem}

.welcome__skiprow{margin:1.35rem 0 0}
.welcome__skip{
  background:none;border:0;border-bottom:1px solid transparent;
  padding:.35rem 0;cursor:pointer;
  font-family:var(--mono);font-size:11px;letter-spacing:.18em;
  text-transform:uppercase;color:var(--dim-2);
  transition:color .25s var(--ease),border-color .25s var(--ease);
}
.welcome__skip:hover{color:var(--ink);border-bottom-color:var(--hair)}

.welcome__status{margin:1.15rem 0 0;font-size:13.5px;line-height:1.55;color:var(--ink-2)}
#welcome-status:empty{display:none}
#welcome-status[data-ok="false"]{color:var(--amber)}

.welcome__alt{
  margin-top:clamp(24px,4vh,38px);
  padding-top:clamp(20px,3vh,30px);
  border-top:1px solid var(--hair);
}
#welcome-auth[hidden]{display:none}
.welcome__altk{margin:0 0 .5rem;color:var(--dim-2)}
.welcome__altd{margin:0 0 1.05rem;font-size:13px;line-height:1.55;color:var(--dim-2);max-width:42ch}
.welcome[data-state="signing"] .welcome__altk{color:var(--ink)}

/* Every control in the frame keeps a ring a keyboard can see. */
#welcome :focus-visible{outline:1px solid var(--accent);outline-offset:3px}
.welcome__input:focus-visible{border-bottom-color:var(--accent)}

@keyframes welcome-in{from{opacity:0}to{opacity:1}}
@media (prefers-reduced-motion:reduce){
  #welcome{animation:none}
}
