@font-face {
  font-family: 'Berkeley Mono';
  font-style: normal;
  font-weight: 100 400;
  font-display: swap;
  src: url(assets/fonts/BerkeleyMonoVariable-Regular.woff2) format('woff2'),
       url(assets/fonts/BerkeleyMonoVariable-Regular.woff) format('woff');
}

:root {
  --bg:#020805; --panel:#04120a; --line:#0e3019; --faint:#0a2412;
  --dim:#2f7d4d; --txt:#4fd67d; --bright:#bcffd0; --accent:#39ff7a; --track:#0c2414;
  color-scheme: dark;
}
* { box-sizing:border-box; margin:0; padding:0; }
html, body { background:#000; font-family:'Berkeley Mono', ui-monospace, monospace; }
body {
  color:var(--txt); font-size:13px; line-height:1.5; padding:12px;
  position:relative; background:var(--bg);
}
body::after {
  content:""; position:fixed; inset:0; pointer-events:none; z-index:50;
  background:repeating-linear-gradient(to bottom, rgba(0,0,0,.16) 0, rgba(0,0,0,.16) 1px, transparent 1px, transparent 3px);
}
.glow { text-shadow:0 0 4px rgba(57,255,122,.45); }
.dashboard { max-width:1500px; margin-inline:auto; }

/* topbar */
.top { display:flex; align-items:center; gap:12px; flex-wrap:wrap; border-bottom:1px solid var(--line); padding-bottom:9px; margin-bottom:11px; }
.brand { color:var(--bright); font-size:16px; letter-spacing:.04em; }
.cur { color:var(--accent); }
.status { color:var(--dim); font-size:11.5px; } .status .on { color:var(--accent); }
.spacer { flex:1; }
.seg { display:inline-flex; border:1px solid var(--line); }
.seg button { background:transparent; border:0; color:var(--dim); font:inherit; font-size:11px; letter-spacing:.06em; padding:3px 9px; cursor:pointer; border-right:1px solid var(--line); }
.seg button:last-child { border-right:0; }
.seg button.on { background:var(--accent); color:#021006; }
.meta { color:var(--dim); font-size:11.5px; } .meta b { color:var(--txt); }

/* presets */
.presets { display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-bottom:11px; }
.grp { color:var(--dim); font-size:11px; letter-spacing:.16em; margin:0 4px 0 2px; }
.chip { border:1px solid var(--line); color:var(--dim); padding:3px 10px; font-size:11.5px; letter-spacing:.08em; text-transform:uppercase; background:transparent; white-space:nowrap; cursor:pointer; }
.chip:hover { color:var(--txt); border-color:var(--dim); }
.chip.on { background:var(--accent); color:#021006; border-color:var(--accent); }

/* bands */
.band { border:1px solid var(--line); background:var(--panel); padding:9px 11px 11px; margin-bottom:11px; }
.ph { color:var(--bright); font-size:11px; letter-spacing:.2em; text-transform:uppercase; display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--line); padding-bottom:6px; margin-bottom:10px; }
.ph .c { color:var(--dim); }
.ph .tog { display:none; color:var(--accent); margin-left:10px; cursor:pointer; }
.ph button.tog { background:transparent; border:0; font:inherit; padding:0; }
.ticker { display:none; }
.boards-backdrop { display:none; }

/* filters */
.fgrid { display:grid; grid-template-columns:repeat(4,1fr); gap:9px 26px; }
.slab .lh { display:flex; justify-content:space-between; font-size:11px; margin-bottom:5px; }
.slab .lh .lbl { color:var(--dim); } .slab .lh .val { color:var(--bright); } .slab .lh .val .u { color:var(--dim); }
.slab .lh .val.active { color:var(--accent); }
.track { position:relative; height:6px; background:var(--track); border:1px solid var(--line); touch-action:none; }
.range { position:absolute; top:0; bottom:0; background:var(--accent); opacity:.65; background-image:repeating-linear-gradient(90deg, rgba(0,0,0,.55) 0 1px, transparent 1px 4px); }
.knob { position:absolute; top:-3px; width:6px; height:10px; background:var(--bright); box-shadow:0 0 4px rgba(57,255,122,.6); cursor:ew-resize; }
.knob:focus { outline:1px solid var(--accent); outline-offset:1px; }
@media (pointer:coarse) { .knob { width:14px; height:18px; top:-7px; } .track { height:8px; } }

/* results table */
.tvp { position:relative; }
.tablewrap { overflow-x:auto; scrollbar-width:thin; }
/* separate, not collapse: collapsed borders belong to the table grid and
   scroll away when thead goes position:sticky; only per-cell bottom borders
   are used so the two modes render identically here */
table { width:100%; border-collapse:separate; border-spacing:0; font-size:12px; }
thead th { color:var(--dim); font-weight:400; text-align:right; font-size:10px; letter-spacing:.08em; text-transform:uppercase; border-bottom:1px solid var(--line); padding:4px 7px; white-space:nowrap; cursor:pointer; user-select:none; }
th.l, td.l { text-align:left; }
th.flex, td.flex { width:100%; }
thead th .ar { color:var(--accent); }
tbody td { padding:3px 7px; text-align:right; border-bottom:1px solid var(--faint); white-space:nowrap; }
tbody tr:hover td { background:#08200f; }
th.city, td.city { position:sticky; left:0; background:var(--panel); z-index:2; }
.tvp.scrollable th.city, .tvp.scrollable td.city { box-shadow:6px 0 8px -4px rgba(0,0,0,.7); }
.name { color:var(--bright); } .u { color:var(--dim); font-size:10px; } .dim { color:var(--dim); } .big { color:var(--bright); }
.g { display:inline-flex; align-items:center; gap:6px; justify-content:flex-end; } .g .n { min-width:20px; text-align:right; }
.meter { width:36px; height:7px; background:var(--track); border:1px solid var(--line); position:relative; display:inline-block; }
.meter i { position:absolute; left:0; top:0; bottom:0; background:var(--accent); opacity:.7; background-image:repeating-linear-gradient(90deg, rgba(0,0,0,.6) 0 1px, transparent 1px 3px); }
.dayrow { display:inline-flex; gap:1px; }
.dayrow b { width:13px; text-align:center; font-weight:400; font-size:10px; color:#1c4a2c; }
.dayrow b.on { color:#021006; background:var(--accent); }
.dayrow b.nd { color:#0d2415; }
.empty { color:var(--dim); padding:18px 7px; }

/* city links in the results table and the boards: the row already reads as a
   city, so the anchor only needs to answer "is this clickable" on hover */
.clink { color:inherit; text-decoration:none; }
.clink:hover { color:var(--accent); text-decoration:underline; }
.clink:focus-visible { outline:1px solid var(--accent); outline-offset:1px; }

/* scroll affordance */
.fade-r { display:none; position:absolute; top:0; bottom:0; right:0; width:54px; pointer-events:none; z-index:3; background:linear-gradient(to right, rgba(4,18,10,0), var(--panel) 78%); }
.swipe { display:none; position:absolute; bottom:6px; right:8px; z-index:4; pointer-events:none; color:var(--accent); font-size:11px; letter-spacing:.1em; text-shadow:0 0 5px rgba(57,255,122,.6); animation:pulse 1.3s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity:.4; } 50% { opacity:1; } }
.tvp.scrollable .fade-r, .tvp.scrollable .swipe { display:block; }

/* boards */
.bgrid { display:grid; grid-template-columns:repeat(4,1fr); gap:10px 22px; }
.sub { color:var(--dim); font-size:10px; letter-spacing:.14em; text-transform:uppercase; margin-bottom:6px; }
.lb .r { display:flex; justify-content:space-between; padding:2px 0; }
.lb .r .nm { color:var(--txt); } .lb .r .nm .rk { color:var(--dim); display:inline-block; width:16px; } .lb .r .v { color:var(--bright); }
.summary .r { display:flex; justify-content:space-between; padding:2px 0; color:var(--txt); } .summary .r .v { color:var(--bright); }

.foot { color:var(--dim); font-size:11px; display:flex; gap:16px; flex-wrap:wrap; padding:2px; }
.foot .on { color:var(--accent); } .foot a { color:var(--dim); }

/* app shell (all sizes): the page itself never scrolls — results scroll
   inside their band, boards stay on screen. Falls back to page scroll on
   very short viewports via the .tablewrap min-height. */
/* the inner chain has min-height:0 so the table can compress to the
   available space (a flex container's intrinsic minimum is its content
   height, which would forbid shrinking); the explicit floor on the band
   itself (~header + 200px of table) is the one true shrink limit — flex
   stops there and the page scrolls instead of content overlapping. */
.band.results { display:flex; flex-direction:column; min-height:254px; }
.tvp { flex:1; min-height:0; display:flex; flex-direction:column; }
.tablewrap { overflow-y:auto; flex:1; min-height:0; }
thead th { position:sticky; top:0; background:var(--panel); z-index:3; }
th.city { z-index:4; } /* corner cell: above sticky header row and sticky city column */

@media (min-width:680px) {
  .dashboard { height:calc(100vh - 24px); } /* 24px = body 12px top+bottom padding */
}

/* medium (680-1079px): single column, results take the leftover height,
   boards sit below in their 2-column grid */
@media (min-width:680px) and (max-width:1079px) {
  .dashboard { display:flex; flex-direction:column; }
  .band.results { flex:1; }
}

/* wide (>=1080px): boards become a right sidebar next to the results */
@media (min-width:1080px) {
  .dashboard {
    display:grid;
    grid-template-columns:minmax(0,1fr) 300px;
    grid-template-rows:auto auto auto 1fr auto; /* 1fr min = content minimum: never crush the results row */
    grid-template-areas:
      "top top"
      "presets presets"
      "filters filters"
      "results boards"
      "foot foot";
    column-gap:11px;
  }
  .top { grid-area:top; }
  .presets { grid-area:presets; }
  .band.filters { grid-area:filters; }
  .band.results { grid-area:results; }
  .band.boards { grid-area:boards; min-height:0; overflow-y:auto; }
  .foot { grid-area:foot; }
  .band.boards .bgrid { grid-template-columns:1fr; } /* boards + summary stack vertically */
}

@media (max-width:1079px) { .fgrid { grid-template-columns:repeat(3,1fr); } .bgrid { grid-template-columns:repeat(2,1fr); gap:14px 22px; } }
/* upper-medium: enough width for 4-across filters and boards — halves their
   height so the boards stay inside the first viewport at ~800px tall
   (after the generic <=1079 block so it wins at 840-1079) */
@media (min-width:840px) and (max-width:1079px) {
  .fgrid { grid-template-columns:repeat(4,1fr); }
  .bgrid { grid-template-columns:repeat(4,1fr); gap:10px 16px; }
}
@media (max-width:679px) {
  body { padding:8px; }
  /* mobile app shell: results scroll internally; the collapsed ticker and
     the footer live in normal flow at the bottom of the viewport. dvh so
     the browser's dynamic toolbar doesn't push the footer off screen. */
  .dashboard { height:calc(100vh - 16px); height:calc(100dvh - 16px); display:flex; flex-direction:column; }
  .band.results { flex:1; }
  .meta { display:none; }
  .presets { flex-wrap:nowrap; overflow-x:auto; padding-bottom:4px; }
  .fgrid { grid-template-columns:1fr 1fr; }
  .bgrid { grid-template-columns:1fr; }
  .ph .tog { display:inline; }
  .hint-drag { display:none; }
  .band.filters.collapsed .ph { border-bottom:0; margin-bottom:0; padding-bottom:0; }
  .band.filters.collapsed .body { display:none; }

  /* boards: in-flow ticker strip between results and footer, expandable to
     a fixed bottom sheet. Collapsed is the class-less default so a fresh
     htmx swap of the band lands collapsed; JS re-adds .expanded if the
     sheet was open. */
  .band.boards {
    margin-bottom:8px; border-color:var(--accent);
    box-shadow:0 0 10px rgba(57,255,122,.25);
  }
  .band.boards:not(.expanded) { padding:0; }
  .band.boards.expanded {
    position:fixed; left:8px; right:8px; bottom:8px; z-index:40; /* under the scanline overlay (z 50) */
    margin:0; /* the in-flow margin-bottom would offset the fixed margin box */
  }
  .band.boards:not(.expanded) .ph,
  .band.boards:not(.expanded) .bgrid { display:none; }
  .band.boards:not(.expanded) .ticker {
    display:flex; align-items:center; gap:7px; width:100%;
    background:transparent; border:0; color:var(--bright); font:inherit;
    font-size:11.5px; letter-spacing:.06em; padding:9px 10px; cursor:pointer; text-align:left;
  }
  .ticker .tk-label { color:var(--dim); letter-spacing:.14em; }
  .ticker .tk-value { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .ticker .tk-pos { color:var(--dim); font-size:10px; }
  .ticker .tk-arrow { color:var(--accent); }
  .band.boards.expanded { max-height:72vh; overflow-y:auto; }
  .band.boards.expanded .ticker { display:none; }
  .boards-backdrop:not([hidden]) { position:fixed; inset:0; z-index:39; background:rgba(0,2,1,.7); display:block; }
}

/* ---- map page ------------------------------------------------------------
   Same shell as the search page: the band fills the leftover height and the
   map fills the band, so the page itself never scrolls. */
.dashboard.mapview { display:flex; flex-direction:column; }
.band.mapband { display:flex; flex-direction:column; flex:1; min-height:320px; }
#map { flex:1; min-height:0; background:var(--bg); border:1px solid var(--line); }

/* view switch in the topbar: anchors styled as the existing segmented control */
#view a { display:inline-block; color:var(--dim); font-size:11px; letter-spacing:.06em; padding:3px 9px; text-decoration:none; border-right:1px solid var(--line); }
#view a:last-child { border-right:0; }
#view a:hover { color:var(--txt); }
#view a.on { background:var(--accent); color:#021006; }

/* The basemap is standard OSM raster run through a filter, rather than a
   second tile provider: invert drops the light cartography to near-black,
   sepia+hue-rotate lands the remaining ink on the site's green, and the last
   two stops pull it back down to something a glow can sit on top of. One
   declaration to tune — nothing else depends on how it's built. */
.basemap { filter:invert(1) sepia(1) hue-rotate(78deg) saturate(2.1) brightness(.62) contrast(1.15); }
/* Screen blending is what makes the clouds read as light rather than as paint:
   they can only brighten what they cover, so coastlines stay visible through
   the densest of them. */
.glowlayer { mix-blend-mode:screen; }
.leaflet-container { background:var(--bg); font-family:inherit; outline-offset:1px; }
.leaflet-control-zoom a {
  background:var(--panel); color:var(--txt); border:1px solid var(--line);
  border-radius:0; font:inherit; line-height:22px; width:24px; height:24px;
}
.leaflet-control-zoom a:hover { background:#08200f; color:var(--accent); }
.leaflet-bar { border:0; box-shadow:none; }

/* Pins: one small dot per city, lit when its forecast satisfies the filters.
   They stay quiet on purpose — the glow is the answer to the search and the
   pins only say where you are, so they brighten on hover rather than
   competing for attention at rest. */
.pin { fill:var(--dim); fill-opacity:.3; stroke:var(--dim); stroke-opacity:.55; stroke-width:.75; }
.pin:hover { fill:var(--txt); fill-opacity:.8; stroke:var(--txt); stroke-opacity:1; }
.pin.match { fill:var(--accent); fill-opacity:.55; stroke:var(--accent); stroke-opacity:.8; }
.pin.match:hover { fill:var(--bright); fill-opacity:1; stroke:var(--bright); stroke-opacity:1; }
.leaflet-tooltip.pintip {
  background:var(--panel); border:1px solid var(--line); border-radius:0;
  color:var(--txt); font-size:11px; letter-spacing:.04em; padding:4px 7px;
  box-shadow:0 0 8px rgba(0,0,0,.6);
}
.leaflet-tooltip.pintip b { color:var(--bright); display:block; font-weight:400; }
.leaflet-tooltip.pintip .c { color:var(--dim); font-size:10px; }
.leaflet-tooltip.pintip::before { border-top-color:var(--line); }

.maplegend { align-items:center; margin-top:9px; }
.maplegend .ramp { display:inline-block; width:70px; height:7px; border:1px solid var(--line); background:linear-gradient(90deg, rgba(47,125,77,.35), rgba(57,255,122,.75), rgba(188,255,208,.95)); }
.maplegend .pin-key { display:inline-block; width:7px; height:7px; background:var(--dim); margin-right:5px; vertical-align:middle; }
.maplegend .pin-key.match { background:var(--accent); box-shadow:0 0 5px rgba(57,255,122,.7); }
#pincount button { padding:2px 7px; font-size:10px; }

@media (max-width:679px) {
  /* the filter band collapses on phones as it does on the search page, so the
     map keeps the height it needs to be worth showing */
  .band.mapband { min-height:260px; }
  .maplegend { font-size:9.5px; gap:8px; }
}

/* ---- city detail page ----------------------------------------------------
   The search page is an app shell pinned to the viewport; this one is a
   document. `.dashboard.detail` outranks the plain `.dashboard` rules on
   specificity alone, so it opts out of the fixed height and the sidebar grid
   at every breakpoint without repeating their media queries. */
.dashboard.detail { height:auto; display:block; }
.detail .band { margin-bottom:11px; }
.back { color:var(--accent); text-decoration:none; font-size:11.5px; letter-spacing:.06em; }
.back:hover { text-shadow:0 0 5px rgba(57,255,122,.6); }
@keyframes blink { 0%,49% { opacity:1; } 50%,100% { opacity:.2; } }
.cur.blink { animation:blink 1.2s steps(1,end) infinite; }
@media (prefers-reduced-motion:reduce) { .cur.blink { animation:none; } }

/* key/value grid, used by the location block and today's figures */
.kgrid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:5px 22px; }
.kv { display:flex; justify-content:space-between; gap:10px; border-bottom:1px solid var(--faint); padding:2px 0; }
.kv .k { color:var(--dim); font-size:11px; letter-spacing:.08em; text-transform:uppercase; white-space:nowrap; }
.kv .v { color:var(--bright); text-align:right; }
.kfoot { display:flex; align-items:center; gap:6px 12px; flex-wrap:wrap; margin-top:9px; font-size:11px; }
.kfoot a { color:var(--accent); text-decoration:none; white-space:nowrap; }
.kfoot a:hover { text-shadow:0 0 5px rgba(57,255,122,.6); }
.kfoot .c { color:var(--dim); letter-spacing:.08em; }

/* today */
.now { display:flex; gap:26px; align-items:flex-start; flex-wrap:wrap; }
.now-main { min-width:190px; }
.now-temp { color:var(--bright); font-size:44px; line-height:1.05; text-shadow:0 0 8px rgba(57,255,122,.35); }
.now-lo { color:var(--dim); font-size:20px; margin-left:8px; }
.now-sky { color:var(--accent); font-size:12px; letter-spacing:.14em; margin-top:4px; }
.now-feel { color:var(--dim); font-size:11px; letter-spacing:.08em; margin-top:2px; }
.now-grid { flex:1; min-width:280px; grid-template-columns:repeat(3,minmax(0,1fr)); }
.parts { display:grid; grid-template-columns:repeat(auto-fit,minmax(230px,1fr)); gap:10px 22px; margin-top:12px; border-top:1px solid var(--line); padding-top:10px; }
.part .sub { color:var(--accent); }

/* hourly strip: one series at a time, one column per hour, scrolling sideways
   on narrow screens. The axis labels sit outside the scroller so they stay put
   while the columns scroll under them — a bar whose baseline isn't zero means
   nothing without them. */
.hmetric { margin-left:10px; }
.hmetric button { padding:2px 7px; font-size:10px; }
.hourwrap { display:flex; align-items:stretch; gap:8px; }
.haxis {
  display:flex; flex-direction:column; justify-content:space-between;
  /* aligns the two labels with the top and bottom of the bar track: the
     column's value label sits above it, the hour label below */
  padding:15px 0 27px; color:var(--dim); font-size:10px; text-align:right;
  white-space:nowrap; flex:none;
}
.hours { display:flex; gap:2px; min-width:max-content; padding-bottom:2px; overflow-x:auto; scrollbar-width:thin; }
.hour { display:flex; flex-direction:column; align-items:center; gap:3px; width:42px; padding:0 2px; }
.hour.brk { border-left:1px solid var(--line); }
.hour .ht { color:var(--dim); font-size:10px; }
.hour.day .ht { color:var(--txt); }
.hour .hbar { position:relative; width:14px; height:74px; background:var(--track); border:1px solid var(--line); display:block; }
.hour .hbar i { position:absolute; left:0; right:0; bottom:0; }
/* night hours stay legible but recede, so the daylight run reads as a shape */
.hour .hbar i { background:var(--accent); opacity:.35; background-image:repeating-linear-gradient(0deg, rgba(0,0,0,.55) 0 1px, transparent 1px 4px); }
.hour.day .hbar i { opacity:.8; }
.hour .hh { color:var(--dim); font-size:10px; }
.hour.day .hh { color:var(--txt); }
.hour .hd { color:var(--accent); font-size:9px; letter-spacing:.06em; height:12px; white-space:nowrap; }
.legend { display:flex; gap:16px; flex-wrap:wrap; margin-top:8px; font-size:10px; letter-spacing:.1em; }
.legend .c { color:var(--dim); }
#hcaption { color:var(--accent); }

/* 10-day table: same skin as the results table, plus a temperature range bar */
table.daily th { cursor:default; }
table.daily tr.today td { background:#08200f; }
table.daily tr.today td.name { color:var(--accent); }
.tbar { position:relative; display:block; width:96px; height:7px; background:var(--track); border:1px solid var(--line); }
.tbar i { position:absolute; top:0; bottom:0; background:var(--accent); opacity:.75; background-image:repeating-linear-gradient(90deg, rgba(0,0,0,.55) 0 1px, transparent 1px 4px); }
th.trange, td.trange { width:96px; }

/* nearby */
.nearby { display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:6px 18px; }
.near { display:grid; grid-template-columns:1fr auto; gap:0 10px; text-decoration:none; border:1px solid var(--line); padding:6px 9px; }
.near:hover { border-color:var(--dim); background:#08200f; }
.near:focus-visible { outline:1px solid var(--accent); outline-offset:1px; }
.near .nm { color:var(--bright); }
.near .v { color:var(--txt); text-align:right; }
.near .c { color:var(--dim); font-size:10.5px; letter-spacing:.06em; }
.near .u { color:var(--dim); font-size:10px; text-align:right; }

.detail .foot { border-top:1px solid var(--line); padding-top:8px; }
.detail .foot a { color:var(--accent); text-decoration:none; }

@media (max-width:1079px) {
  .kgrid, .now-grid { grid-template-columns:repeat(2,minmax(0,1fr)); }
}
@media (max-width:679px) {
  .kgrid, .now-grid { grid-template-columns:1fr; }
  .now-temp { font-size:36px; }
  .detail .top { gap:8px; }
}
