/* Para Cast — mobile-first map board of Canada RASP windgrams.
   Layout is a fixed top bar over a full-bleed map; everything else floats. */

:root {
  --bg: #0f141b;
  --panel: rgba(17, 24, 33, 0.92);
  --line: #2b3644;
  --text: #e8eef6;
  --muted: #93a3b6;
  --accent: #4bb3fd;
  /* The same blue faded back, for the references under each section heading.
     They are the one thing in an opened site that is there to be followed
     rather than read, so they have to be plainly links and plainly not the
     point: the accent softened toward this page's muted grey keeps the hue
     that says "way out of here" while giving up the brightness that would
     have a line of small print outrank the picture it credits. */
  --accent-faded: #64ade4;
  --warn: #f0a05a;
  /* The AQHI's own risk bands. The first two are the blue and the amber this
     page already speaks in; only the two above them need a colour of their
     own, and they are set bright rather than the index's printed maroon,
     which disappears into a dark panel. */
  --aqhi-low: var(--accent);
  --aqhi-moderate: var(--warn);
  --aqhi-high: #ff6b5a;
  --aqhi-very-high: #ff4d6d;
  /* The edge a card wears when its day's air is worth knowing about before the
     site is opened. One colour for every band, because the outline answers
     "is there something here" and the band below it answers "how bad"; three
     shades of outline would be a scale nobody could read at thumbnail size.
     Grey, because grey is what the thing being reported looks like and because
     it is the one neutral this map has not already spent: orange belongs to the
     fire, blue to the page's own chrome. Light, but the dark end of light - the
     mark is laid half inside a white card and half outside it on a canvas of
     rgb(71, 71, 73), so it has to be found against both, and a paler grey keeps
     only the outer half.

     A step darker than it first was, which is that same rule read from the side
     it was short on. At #b0b0b0 the half outside stood at 4.3:1 on the land and
     7.3:1 over the water while the half lying on the card's own white had
     2.2:1 - under the 3:1 a mark carrying meaning is meant to clear, and it is
     the half a reader looking straight at a thumbnail is looking at, since the
     outer half is a ring in the gap between cards and the inner one runs along
     the plot. Here those are 3.5:1, 6.0:1 and 2.6:1: the outer half spends
     contrast it had to spare - down to where the fire's orange already sits,
     and still nowhere near the 2.3:1 the black this replaced was rejected at -
     and the inner half is bought with it. Not darker than this, because the
     inner half only reaches 3:1 at a grey that drops the outer one to 3.1:1,
     which is trading a mark that is thin against white for one that is thin
     against the layer the map opens on.

     Neutral, with none of the page's blue mixed in: a tint nobody can see at
     thumbnail size is contrast spent for nothing. */
  --air-edge: #a0a0a0;
  /* Rain, in the two steps the card's edge reports it in. Blue because it is
     the one thing this app warns about that is not smoke or fire, and because
     water is the colour a reader will guess without being told. The likely one
     is dark enough to hold its own against a pale plot; the possible one is the
     same hue lifted until it reads as the lesser of the two at a glance and
     from across a map, which is the whole job of having two. */
  --rain-high-edge: #1e63d6;
  --rain-low-edge: #7fb6f2;
  /* The edge of a card standing inside a region Environment Canada has issued
     an alert over. Red, because red is the one thing a colour can say without
     being taught it, and this is the only mark on the map that is somebody
     else's warning rather than this app's reading of a number — a forecaster
     decided it was worth telling people about, which is a different kind of
     claim from an index crossing a threshold.

     Light for a red, and that is the same trade the air edge made when it gave
     up black. The map opens on ground of rgb(71, 71, 73), and red is a dark
     hue: a full-strength one stands at 2.5:1 there, which on a chip — where
     this ring is the whole of the warning — is a warning that cannot be found.
     Lifted to here it is 3.0:1 on that ground and 5.2:1 over water, alongside
     the fire orange's 3.4:1, and the lightness is also what keeps it apart
     from that orange when a card is wearing both: hue alone, at two pixels,
     is not much to tell a warning by. */
  --alert-edge: #ff5c5c;
  /* Wind gusts, in the two steps the card's edge reports them in. Violet
     because it is the hue this map has not spent: orange is the fire's, blue
     the rain's and the page's own chrome, grey the smoke's, red the issued
     alert's, and green is what every chart here paints its good hours in.
     Nothing is the colour of wind the way blue is the colour of water, so what
     is left to choose on is distinctness - at 2px across a board of thumbnails,
     the one thing this mark must not do is read as one of the others. Ranked
     the way the blues are: the strong step dark enough to be found against a
     pale plot, the lesser one lifted until it reads as the lesser at a
     glance. */
  --gust-high-edge: #b14ae8;
  --gust-low-edge: #cba0f0;
  /* How far a warning reaches outside the card it marks — one width for both
     of them, so the grey for bad air and the orange for a fire nearby stay
     the two claims of equal standing they were set up as, told apart by their
     colour rather than by how far each one reaches. It is a third less than
     either of them used to take: these rings are drawn into the 6px the layout
     keeps between two cards, and half of that gap was more edge than either
     warning needs to be found on a map whose subject is the plots the edges
     are drawn around. At a third of it a warned card is still plainly marked,
     and two warned neighbours now show a strip of map between their rings
     instead of meeting in the middle of it. */
  --warn-ring: 2px;
  /* The wash a warning sits on inside an opened site. Orange because orange is
     the colour this app warns in — the fire's ring, its flame, the edge of every
     card near one — so the banner is known for a warning before a word of it is
     read, on a view that is otherwise cold and near black. On the map that
     orange is the fire's alone and bad air is marked in black instead, because
     two marks sit side by side on one small card there and colour is all that
     tells them apart; in the banner nothing else is orange, both warnings share
     the one wash, and which kind each line is stays where it always was — its
     rule and its opening words. Deliberately taken right down towards black
     rather than drawn at the ring's own strength: everything printed on this is
     itself a colour that means something — the AQHI's risk band, the fire's
     orange, the blue of the way through to the fire's own page — and a bright
     wash would swallow all three and leave the banner saying only "orange".
     Dark, and fully saturated with it, so what carries the colour is the hue
     rather than the light: at this depth every one of those still clears
     4.5:1 on it. */
  --warn-bg: #401e00;
  --topbar-h: 88px;
}

/* A warning band as a colour, in one place, so the chip along the foot of a
   card on the map and the line inside an opened site are never two different
   reds for one reading. Only these three exist: "low" is not a warning. */
.aqhi-moderate { --air-tone: var(--aqhi-moderate); }
.aqhi-high { --air-tone: var(--aqhi-high); }
.aqhi-very-high { --air-tone: var(--aqhi-very-high); }

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.35 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  overscroll-behavior: none;
}

/* ------------------------------- top bar -------------------------------- */
#topbar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 1200;
  padding: max(6px, env(safe-area-inset-top)) 8px 6px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  backdrop-filter: blur(10px);
}

/* The two arrows sit immediately either side of the day label, so the whole
   control reads as one unit and both arrows stay under the same thumb. */
.datebar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}

.daynav {
  flex: 0 0 auto;
  width: 46px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 26px;
  line-height: 1;
  color: var(--text);
  background: #1b2430;
  border: 1px solid var(--line);
  border-radius: 10px;
}

.daynav:disabled { opacity: 0.35; }

.daylabel {
  flex: 0 1 auto;
  min-width: 0;
  height: 42px;
  padding: 0 16px;
  display: flex;
  align-items: center;   /* centred in the box; baseline sat the text too high */
  justify-content: center;
  gap: 8px;
  color: var(--text);
  background: #1b2430;
  border: 1px solid var(--line);
  border-radius: 10px;
  font-size: 17px;
  font-weight: 600;
  line-height: 1;
}

.daydate { font-size: 13px; font-weight: 400; color: var(--muted); }
.daycaret { font-size: 10px; color: var(--muted); }

/* The to-do button sits out of the flow at the bar's right edge, and the date
   control keeps a matching gutter on the left, so the day stays centred. */
.datebar { padding: 0 46px; }

.todobtn {
  position: absolute;
  right: 8px;
  top: max(6px, env(safe-area-inset-top));
  width: 42px;
  height: 42px;
  font-size: 20px;
  line-height: 1;
  color: var(--text);
  background: #1b2430;
  border: 1px solid var(--line);
  border-radius: 10px;
}

.todobtn[aria-expanded="true"] { color: var(--accent); border-color: var(--accent); }

/* Day picker: the whole scrollable range in one tap, rather than making you
   step through it with the arrows. */
.daymenu {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: calc(100% - 4px);
  z-index: 1300;
  width: min(320px, calc(100vw - 24px));
  max-height: min(60vh, 420px);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 4px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 12px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.65);
}

.daymenu[hidden] { display: none; }

.daymenu-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  width: 100%;
  padding: 10px 12px;
  background: transparent;
  border: 0;
  border-radius: 8px;
  color: var(--text);
  font: inherit;
  text-align: left;
}

.daymenu-item + .daymenu-item { margin-top: 2px; }
.daymenu-item:hover { background: #1b2430; }

.daymenu-item.current {
  background: rgba(75, 179, 253, 0.16);
  box-shadow: inset 0 0 0 1px rgba(75, 179, 253, 0.45);
}

.daymenu-name { font-weight: 600; }
.daymenu-date { font-size: 12px; color: var(--muted); }

.statusbar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 2px 0;
  font-size: 12px;
  color: var(--muted);
}

.badge {
  flex: 0 0 auto;
  padding: 2px 8px;
  border-radius: 999px;
  background: rgba(75, 179, 253, 0.16);
  border: 1px solid rgba(75, 179, 253, 0.45);
  color: var(--accent);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.badge.empty {
  background: rgba(240, 160, 90, 0.14);
  border-color: rgba(240, 160, 90, 0.45);
  color: #f0a05a;
}

.status {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.iconbtn {
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  font-size: 17px;
  color: var(--text);
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 8px;
}

.iconbtn.busy { opacity: 0.4; }

/* --------------------------------- map ---------------------------------- */
#map {
  position: fixed;
  inset: var(--topbar-h) 0 0 0;
  background: var(--bg);
}

.leaflet-container { background: var(--bg); font: inherit; }
.leaflet-control-attribution { font-size: 9px; background: rgba(15, 20, 27, 0.7); color: var(--muted); }
.leaflet-control-attribution a { color: var(--muted); }

.fab {
  position: fixed;
  right: 12px;
  bottom: calc(18px + env(safe-area-inset-bottom));
  z-index: 1100;
  width: 46px;
  height: 46px;
  font-size: 20px;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 50%;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
}

.fab.active { color: var(--accent); border-color: var(--accent); }

.fab-second { bottom: calc(72px + env(safe-area-inset-bottom)); }

/* ------------------------- windgram thumbnails --------------------------- */
/* Each marker icon is a zero-size point pinned at the site's coordinates; the
   card hangs off it, so resizing the card never moves the anchor point. */
.tile { position: relative; width: 0; height: 0; }

.tile-dot {
  position: absolute;
  left: 0;
  top: 0;
  width: 9px;
  height: 9px;
  margin: -5px 0 0 -5px;
  border-radius: 50%;
  background: var(--accent);
  border: 1.5px solid #0f141b;
  box-shadow: 0 0 0 1px rgba(75, 179, 253, 0.5);
}

/* Leader line from a site's dot to its thumbnail, for cards that had to be
   moved off their own position to avoid covering a neighbour. */
.tile-leader {
  position: absolute;
  left: 0;
  top: 0;
  width: 0;
  height: 0;
  display: none;
  border-top: 2px solid rgba(75, 179, 253, 0.9);
  transform-origin: 0 0;
  pointer-events: none;
  filter: drop-shadow(0 0 1.5px rgba(0, 0, 0, 0.75));
}

.tile.displaced .tile-leader { display: block; }
.tile.displaced.focused .tile-leader { border-top-color: #ffffff; }

/* No padding of its own: the plot runs right out to the card's border, and each
   row under it carries whatever inset its own text needs. The white ring that
   used to sit between the two was chrome charged to the one thing on the card
   worth looking at - a fixed 3px, so on the small cards it was several percent
   of the picture - and the border already parts the card from the map without
   it. `overflow: hidden` is what lets the plot do that: it hands the picture,
   and the warning bands along the foot, the card's own rounded corners at
   whatever radius its border leaves, so neither has to name a radius that would
   be wrong on a chip or on a warned card's thicker edge. */
.tile-card {
  position: absolute;
  left: 0;
  bottom: 10px;
  transform: translateX(-50%);   /* re-centres itself whenever width changes */
  width: 100px;
  background: rgba(255, 255, 255, 0.94);
  border: 1px solid rgba(75, 179, 253, 0.75);
  border-radius: 6px;
  overflow: hidden;
  /* Every ring this card can wear is drawn here, once, and the rules further
     down say only what colour it is and how far it reaches. Two names, because
     two of them can be true at the same time: `--ring-tone` is whatever the
     card is warned about by this app's own reading of the day, and
     `--alert-tone` is an alert Environment Canada issued — which outranks all
     of them and so is written over the top of whichever one won. They are
     separate properties rather than one both sides set, because which rule
     wins a shared property is settled by how many classes each selector
     happens to name, and the standing of a warning is not a thing to leave to
     that. */
  box-shadow: 0 0 0 var(--ring-reach, 0px)
              var(--alert-tone, var(--ring-tone, transparent)),
              var(--card-shadow, 0 3px 10px rgba(0, 0, 0, 0.55));
  transition: width 180ms ease-out;
  cursor: pointer;
}

/* Leaflet forces `width: auto` on any img inside the marker pane (to stop page
   styles breaking its tiles), and that selector outranks a plain `.tile-card
   img`. Out-specify it, or every thumbnail renders at its intrinsic 880px.
   Both rules below target their own class so the plot and the weather icon
   never compete over width. */
.leaflet-container .leaflet-marker-pane img.tile-plot {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;   /* overridden per site; reserves space before load */
  background: #fff;
}

/* The side inset here and on the weather strip below is the card's, now that the
   card keeps none of its own: enough to hold text off the border, and no more. */
.tile-name {
  padding: 1px 4px 2px;
  color: #16202b;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The day's weather at the site's own coordinates. The compact two-line strip
   rides under the name on any thumbnail with room for it; the full reading
   appears when focused. */
.tile-wx {
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 2px 4px 4px;
  text-align: left;
}

.leaflet-container .leaflet-marker-pane img.tile-wx-icon {
  flex: 0 0 auto;
  display: block;
  width: 26px;
  height: 26px;
  aspect-ratio: auto;
  background: transparent;
}

.tile-wx-lines { flex: 1 1 auto; min-width: 0; }

.tile-wx-top {
  display: flex;
  align-items: baseline;
  gap: 5px;
  min-width: 0;
}

.tile-wx-temp {
  flex: 0 0 auto;
  color: #16202b;
  font-size: 13px;
  font-weight: 800;
  line-height: 1.15;
}

/* "Low", against the number it qualifies: small enough that marking a night
   reading costs the conditions beside it almost none of their room. */
.tile-wx-qual {
  margin-right: 1px;
  font-size: 9.5px;
  font-weight: 700;
  color: #5c6b7d;
}

.tile-wx-cond {
  flex: 1 1 auto;
  min-width: 0;
  color: #16202b;
  font-size: 10px;
  font-weight: 600;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile-wx-sub {
  color: #5c6b7d;
  font-size: 9.5px;
  font-weight: 600;
  line-height: 1.25;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile.tiny .tile-wx,
.tile.no-wx .tile-wx { display: none; }

/* Focused: the site you asked for, lifted clear of its neighbours. Said in the
   same two properties every other ring on this card is said in, so a focused
   card that is also warned still shows the warning's colour rather than the
   chrome's: what the pointer is doing is not news beside what the day is. */
.tile.focused .tile-card {
  border-color: #ffffff;
  --ring-tone: rgba(75, 179, 253, 0.9);
  --ring-reach: 2px;
  --card-shadow: 0 8px 22px rgba(0, 0, 0, 0.7);
}

.tile.focused .tile-dot {
  width: 13px;
  height: 13px;
  margin: -7px 0 0 -7px;
  background: #fff;
  box-shadow: 0 0 0 2px var(--accent);
}

/* Collapsed: lost the collision test at this zoom, so only the pin remains. */
.tile.collapsed .tile-card { display: none; }

/* Chip size: the plot alone, with the name and chrome stripped back so a tiny
   thumbnail stays a readable splash of colour rather than a squashed label. */
.tile.tiny .tile-name { display: none; }

.tile.tiny .tile-card {
  border-radius: 3px;
  border-width: 1px;
}

.tile-pin {
  position: absolute;
  left: 0;
  bottom: 8px;
  transform: translateX(-50%);
  display: none;
  max-width: 120px;
  padding: 1px 6px;
  background: rgba(17, 24, 33, 0.88);
  border: 1px solid var(--line);
  border-radius: 999px;
  color: var(--text);
  font-size: 10px;
  font-weight: 600;
  white-space: nowrap;
}

.tile.collapsed .tile-pin { display: block; }

/* No plot published for this site today: the card carries only its forecast, so
   the name takes the top space the picture would have had, rather than opening
   on a line of text jammed against the border. */
.tile.no-plot .tile-card {
  background: rgba(255, 255, 255, 0.93);
}

.tile.no-plot .tile-name {
  padding-top: 5px;
  font-size: 11px;
}

/* ------------------------------ launch mark ------------------------------ */
/* This app's own line on somebody else's chart, at the height the launch
   stands. Shared by the two places a windgram is drawn — the thumbnail on the
   map and the picture in the expanded view — and positioned as a fraction of
   whichever of them it is on, so the mark cannot drift between the card that
   was tapped and the view that opens.

   Deliberately in this app's own chrome colours rather than in anything
   canadarasp draws with: a windgram is already a picture full of coloured
   bands and contour lines, and a mark that looked like one more of them would
   read as part of the forecast instead of as the one line on it that is about
   the hill. Thin, because what this mark is for is what lies *on* it — the
   barbs, the shading, whether the inversion is above you — and every row of
   rule is a row of that covered up; a rule only has to be found, not read.

   Two rows of it rather than one. A windgram at full size is 880px of chart
   drawn in lines one row thick — its contours, its grid, its barbs — so a
   one-row mark was the same weight as everything else on the picture and had
   nothing but its colour to be told apart by, which is a thing you check rather
   than a thing you see. Weight is what makes a line stand out of a chart
   already full of lines, and the second row buys it for almost nothing: the
   model levels this plot is drawn on sit some 40-55px apart at full size, so
   the extra row hides about a fortieth of the band between two of them.

   And the two rows are the whole of it: no white ring outside, which cost two
   more rows of hidden chart and worked against the mark on both counts. Light
   on a light picture, first — a windgram is pastel nearly everywhere, and white
   clears 3:1 against none of the four colours most of one is made of, where
   this dark clears 6:1 against all of them. And white is what canadarasp draws
   its wind barbs in, so where the halo could be seen at all it was dressing
   this app's rule in the chart's own ink. What the mark spends on being found
   it spends in its own dark.

   The height below is the expanded view's, where the picture is at full size.
   How much chart the same mark costs on a card is the one thing the two places
   differ on, and the card halves it — see `.tile .plot-line`. */
.plot-line {
  position: absolute;
  height: 2px;
  /* Half its own height, so the rule straddles the launch's row rather than
     hanging under it: the fraction the server sends is where the height falls,
     not where the top of a mark goes. Both numbers move together. */
  margin-top: -1px;
  background: var(--bg);
  /* A mark rather than a control, in both places it is drawn: the card opens
     its site on a click and the overlay closes on one, and pointing at the mark
     must not be different from pointing at the plot it is on. */
  pointer-events: none;
}

.plot-line[hidden] { display: none; }

/* Named on the line itself: an unlabelled rule across somebody's chart is a
   puzzle, and neither place it is drawn has the answer beside it — the height
   it marks is printed nowhere in the expanded view, and a card on the map has
   no room to print it. Held at the left end, where a windgram's first hour
   leaves the most room. Set small, and tight around its one word, for the same
   reason the rule is thin: the chip is opaque, so its size is measured in
   forecast it hides. It only has to be read once — whose mark this is — while
   the chart under it is read over and over. */
.plot-line-label {
  position: absolute;
  top: 50%;
  left: 6px;
  transform: translateY(-50%);
  padding: 1px 4px;
  border-radius: 3px;
  background: var(--bg);
  color: var(--text);
  font-size: 9.5px;
  line-height: 1.4;   /* its own, the wrapper above having none to inherit */
  font-weight: 600;
  white-space: nowrap;
}

/* What the mark is positioned against on a card: exactly the box the picture is
   drawn in, the way `.viewer-plot` is in the expanded view. It cannot be the
   card itself — that carries the name, the weather and the warning bands under
   the picture too, and a fraction of the card is not a fraction of the chart.
   `line-height: 0` so this box is the image's own height rather than a line box
   a couple of pixels taller than it. */
.tile-shot {
  position: relative;
  line-height: 0;
}

/* And on a card the same rule is drawn at half that width: one row against the
   expanded view's two. A thumbnail is the same 880px windgram shrunk to a
   hundred-odd, so every line canadarasp drew is scaled down into a fraction of a
   pixel while this app's own is scaled not at all — at the width above, the mark
   that covers one part in 440 of the picture at native size would cover one part
   in thirty of the narrowest card that draws it, over the very chart it was put
   there to point at, and would be the one thing on a card that had not been
   asked to shrink with the card.

   Half is as far down as it is worth taking, because it is as far as a screen
   can follow — and a whole row is a width every screen draws exactly. A box
   thinner than one device pixel is rounded up to one and painted solid rather
   than faded out, so while the card's share was half a pixel a 1x display was
   already painting it as a full row and had nothing thinner to give: at one row
   that display is unchanged and the denser ones draw in two physical rows where
   they drew one. It costs no contrast anywhere. Nothing else about the mark
   moves with the width — same colour, same place, same fraction of the
   picture. */
.tile .plot-line {
  height: 1px;
  margin-top: -0.5px;   /* half its height, on the y the server placed it at */
}

/* On a thumbnail the rule goes wherever the card's own name goes. Below that
   the plot is a colour chip with a couple of pixels to the model level — half of
   which the rule would take, even at the card's halved width — so it would cover
   the only thing a chip has to say while pointing at nothing on it that can be
   read. */
.tile.tiny .plot-line { display: none; }

/* And it is named only once the card is wide enough for the word to cost the
   picture little. The label is drawn at one fixed size however small the card
   is, so on a narrow thumbnail it is a third of the picture's width spent
   saying what the identical mark on the next card along has already said —
   over the very chart the mark exists to point at. That width is also the one
   at which few enough cards fit on a screen for the word not to come out as a
   chorus. Under it the rule stands unlabelled, and the card's hover text says
   what it is and how high. */
.tile.no-line-label .plot-line-label { display: none; }

/* --------------------------- full-size viewer ---------------------------- */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 2000;
  display: flex;
  flex-direction: column;
  background: #0b0f14;   /* opaque: the map must not ghost through the plot */
  cursor: zoom-out;      /* the whole overlay dismisses back to the map */
}

.viewer[hidden] { display: none; }

.viewer-bar {
  position: relative;   /* the day menu hangs off the bar, not off the page */
  display: flex;
  align-items: center;
  gap: 10px;
  padding: max(8px, env(safe-area-inset-top)) 12px 8px;
  background: #0b0f14;
  border-bottom: 1px solid var(--line);
}

/* The name gives up its width first: it is the one thing here that can be read
   half-said, and the day control beside it must not be pushed off the bar by a
   long one. */
.viewer-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-weight: 600;
}

/* The day, and the step to the next one, immediately beside each other so the
   pair reads as one control - the same reason the top bar keeps its arrows
   tight against its date. Scaled off the close button rather than off that
   bar's 42px picker: this is a title bar, and a control that size in it would
   outweigh the site name it stands next to. */
.viewer-day {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 6px;
}

.viewer-daylabel,
.viewer-daynav {
  flex: 0 0 auto;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--text);
  background: #1b2430;
  border: 1px solid var(--line);
  border-radius: 8px;
  line-height: 1;
  /* Set rather than left to the browser's button font: unlike the top bar's
     picker this one stands directly against the site's name, and two faces on
     one line read as two bars. */
  font-family: inherit;
  /* The overlay puts a zoom-out cursor on everything inside it, and these are
     not a way out of it. */
  cursor: pointer;
}

.viewer-daylabel {
  min-width: 0;
  gap: 6px;
  padding: 0 10px;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
}

.viewer-daylabel[aria-expanded="true"] { border-color: var(--accent); }

/* A notch down from the top bar's, the whole control being a notch smaller. */
.viewer-daylabel .daydate { font-size: 12px; }

.viewer-daynav {
  width: 30px;
  font-size: 20px;
}

.viewer-daynav:disabled { opacity: 0.35; cursor: default; }

/* Pinned to the bar's right-hand edge instead of centred on the control, so on
   a phone - where the menu is nearly the width of the screen - it still lands
   inside it. */
.viewer-daymenu {
  left: auto;
  right: 12px;
  transform: none;
  cursor: default;
}

.viewer-daymenu .daymenu-item { cursor: pointer; }

/* The scrolling part of the view. The foot is deeper than the rest of the
   padding because the last section of a site is a picture — a camera frame, a
   wind trace — and a picture ending flush against the bottom edge of the phone
   reads as one that has been cut off. The references that used to close the
   view carried that space; they are under their own headings now, so the body
   carries it. */
.viewer-body {
  flex: 1 1 auto;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  padding: 8px 8px 24px;
}

.viewer-body img {
  display: block;
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  background: #fff;
  border-radius: 6px;
}

.viewer-body img[hidden] { display: none; }

/* The box a picture is arriving into, and the word that stands in it meanwhile.
   Every picture in this view is fetched after the view is already up, and an
   <img> with nothing in it yet is not a blank picture but no picture at all:
   zero pixels tall, so the section it belongs to reads as empty and everything
   under it sits where it is not going to stay. The camera frames are the worst
   of them — one off the Woodside launch runs to about a megabyte — which is
   why they are the ones this was wanted for, but the windgram, the tide curve
   and the wind traces all arrive late enough to be waited on.

   So each picture gets a box and a word in it. The stand-in is opaque, and in
   the page's own background rather than the white every one of these images is
   backed with: the white belongs to the picture, and a picture drawing itself
   in behind a word that says it has not arrived reads as neither. It is laid
   over the box rather than above it, so nothing has to move out of the way when
   the picture lands. Where the shape is known in advance — the windgram, which
   the day payload publishes an aspect for — the box is the picture's exact box
   and nothing below it moves at all; where it is not, the box is only deep
   enough to hold the word, and that is also what holds it when a picture turns
   out not to be coming. */
.viewer-shot { position: relative; }
.viewer-plot.holding, .viewer-shot.holding { min-height: 64px; }

.viewer-wait {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  background: var(--bg);
  color: var(--muted);
  font-size: 13px;
  /* `.viewer-plot` sets none, having only pictures in it until now. */
  line-height: 1.3;
}

/* `display: flex` above would otherwise outrank the browser's own rule for a
   hidden element, and the plot's stand-in is hidden rather than thrown away. */
.viewer-wait[hidden] { display: none; }

/* And what a section says in words, where it has no picture's box to say it in:
   while its answer is in flight, and again if the answer comes back empty. The
   forecast carried one of these alone while it was the only section that waited
   on a request of its own; now that every section is headed before its answer
   is in, they all say it in the same shape. */
.viewer-note { padding: 10px 2px; color: var(--muted); font-size: 13px; }

/* What each part of an opened site is, said above it. The view is a column of
   pictures from six different services — the windgram, the day's forecast, the
   tide at a landing beach, up to three live traces, then a camera or two — and
   it runs to several screens of a phone, so the headings are both what
   names a picture and what parts one section from the next: the rule under each
   is the line the eye stops at on the way down.

   One notch above the strip labels inside a section and one below the site's
   name in the bar, so the view reads at three weights rather than two. The
   cursor is set because the overlay puts a zoom-out one on everything it
   contains, and a heading is text to read rather than another way out — the
   references trailing on the end of it set their own back, being the one part
   of this line that really is a way out. */
.viewer-head {
  max-width: 900px;
  margin: 14px auto 8px;
  padding: 0 2px 5px;
  border-bottom: 1px solid var(--line);
  color: var(--text);
  font-size: 13px;
  font-weight: 600;
  cursor: default;
}

.viewer-head[hidden] { display: none; }

/* The warning banner, at the head of the view above the plot it is not about.
   Everything this site is warned about stands here — the air over the hill and
   whatever is burning near it — because both answer the same question, and it
   is the one asked before any of the weather below is read. It shares the
   plot's column so the view still reads as one measure wide. The cursor is set
   because the overlay puts a zoom-out one on everything it contains, and this
   is text to read rather than another way out. */
.viewer-warn {
  max-width: 900px;
  margin: 0 auto 10px;
  cursor: default;
}

.viewer-warn[hidden] { display: none; }

/* Each warning is a line with a rule down its side rather than a slab: one
   shape for both keeps them reading as the same kind of thing, stacked, and the
   colour of the rule is the only thing that says which kind.

   Both stand on the warning orange, which is the whole banner's claim on the
   eye: it used to wear the same near-black as the hour cards further down the
   view, so the one block here that can call off the drive looked like every
   other panel and was read, if at all, in its turn. Colour still says which
   kind of warning this is, because the background is now saying only that there
   is one. */
.viewer-warn-row {
  padding: 7px 10px;
  border-left: 3px solid var(--line);
  border-radius: 0 6px 6px 0;
  background: var(--warn-bg);
  color: var(--text);
  font-size: 12.5px;
  line-height: 1.35;
}

.viewer-warn-row + .viewer-warn-row { margin-top: 6px; }

/* The band's own colour, which is the colour that number wears in the hourly
   strip further down and on the card that sent the reader here. */
.viewer-air { border-left-color: var(--air-tone); }
.viewer-air b { color: var(--air-tone); }

/* The rain row, ruled in the step its own figure reached, so the banner and the
   border a reader arrived from are the same colour. */
.viewer-rain.rain-high { border-left-color: var(--rain-high-edge); }
.viewer-rain.rain-high b { color: var(--rain-high-edge); }
.viewer-rain.rain-low { border-left-color: var(--rain-low-edge); }
.viewer-rain.rain-low b { color: var(--rain-low-edge); }

/* The gust row, in the two violets the card's edge uses, for the same reason. */
.viewer-gust.gust-high { border-left-color: var(--gust-high-edge); }
.viewer-gust.gust-high b { color: var(--gust-high-edge); }
.viewer-gust.gust-low { border-left-color: var(--gust-low-edge); }
.viewer-gust.gust-low b { color: var(--gust-low-edge); }

/* Where a row was read, under the line it belongs to. This banner is the one
   block of the view without a heading to gather references under, so each row
   carries its own — the way the alert's head and each fire's record always
   have, and the way every section below is credited directly under its heading.

   Under the sentence rather than trailed on the end of it. A warning row is
   read in a glance and is already a run of clauses parted by dots; a credit set
   inside that run reads as one more of them, which is what took the community
   and its distance off the air row. Set in the muted weight the alert's own
   words and each fire's record take, it is plainly the small print of the line
   above: the warning is what gets read, the way to check it is what waits
   underneath. It reaches 5.7:1 on the banner's orange, and the reference in it
   6.1:1. */
.viewer-warn-src { margin-top: 3px; color: var(--muted); font-size: 11.5px; }
/* At that line's own size, exactly as the two references already in this banner
   take theirs: a way out set as the end of the small print it belongs to rather
   than as something else standing beside it. */
.viewer-warn-src .viewer-ref { font-size: inherit; }

/* What the launch-height mark is positioned against. It has to be exactly the
   box the plot is drawn in — same width rule as the image it wraps, no padding
   of its own — because the mark's place on the chart is given as a fraction of
   that picture and resolved against this. The mark itself is styled with the
   thumbnails' copy of it, further up: it is one mark drawn in two places. */
.viewer-plot {
  position: relative;
  max-width: 900px;
  margin: 0 auto;
  /* No stray line box: with the plot hidden — a day this site has none for —
     the whitespace around the image would otherwise leave a blank line here. */
  line-height: 0;
}

/* Where all of this came from, said section by section: the references for a
   part of the view, trailing on the end of the heading that names it. They were
   gathered into one block at the foot for a while, which kept the small print
   out of the reading but left a reader who wanted one to scroll past
   everything and then work out which of a dozen lines belonged to the picture
   in front of them. Under the heading, each was beside the thing it credits —
   and cost the view a line of a phone's screen per section, set between the
   word and the picture it names, where a run of blue print reads as the
   section's opening statement rather than as its small print.

   So it rides on the heading's own line, and it is plain inline text rather
   than a box put there: a run that laid itself out as one unit would be
   bumped whole onto a second line the moment it outgrew what was left beside
   the word, which on a phone is most headings — and a credit knocked below the
   title is the line under the heading all over again. Flowing, the first source
   stays beside the word it belongs to and only the overflow wraps. Each entry
   is kept unbroken, since half a host name at the end of a line names nothing.

   Normal weight against the heading's 600, so the word being scrolled for is
   still the first thing on the line. No cursor of its own: the heading around
   it already sets the one this overlay would otherwise put on everything, and
   each entry sets a pointer back. */
.viewer-refs { font-weight: 400; }

.viewer-refs[hidden] { display: none; }

.viewer-refs .viewer-ref { margin-left: 14px; white-space: nowrap; }

.viewer-ref {
  color: var(--accent-faded);
  font-size: 12px;
  text-decoration: none;
  cursor: pointer;
}

/* A source with no one page to send anybody to — the forecast is read from an
   API rather than off a city page — so it is credited in the muted weight of
   the text around it rather than dressed as a link that goes nowhere. */
.viewer-ref.plain { color: var(--muted); cursor: default; }

/* The tide chart for a site's landing beach, between the day's forecast and the
   live traces. Spaced like the wind traces below it and for the same reason:
   `.viewer-body img` already sizes and centres any image in here, so the figure
   carries the margin and a `.viewer-tide img` rule that would only tie with it
   on specificity is avoided. The gap rides between two charts rather than above
   each one, the first sitting straight under its own heading. */
.viewer-tides { max-width: 900px; margin: 0 auto; }
.viewer-tides[hidden] { display: none; }
.viewer-tide { margin: 0; }
.viewer-tide + .viewer-tide { margin-top: 10px; }

/* The cameras looking at the site's air, above the traces from the instruments
   standing in it. `.viewer-body img` already sizes and centres any image in
   here, so the figure carries the spacing and the picture is left alone — a
   rule of the form `.viewer-cam img` would tie with it on specificity and be
   decided by source order, which is exactly the fragility the plot images warn
   about. The gap rides between two frames rather than under each, the way the
   charts above and below space themselves: with a section beneath this one, a
   margin on the last frame is a gap belonging to nothing. */
.viewer-cams { max-width: 900px; margin: 0 auto; }
.viewer-cams[hidden] { display: none; }
.viewer-cam { margin: 0; }
.viewer-cam + .viewer-cam { margin-top: 10px; }

/* The wind station traces at the foot of the view, one per instrument near the
   site. Like the cameras above, the spacing rides on the figure rather than the
   picture: `.viewer-body img` already sizes and centres any image in here, and
   a `.viewer-wind img` rule would only tie with it on specificity. */
.viewer-winds { max-width: 900px; margin: 0 auto; }
.viewer-winds[hidden] { display: none; }
.viewer-wind { margin: 0; }
.viewer-wind + .viewer-wind { margin-top: 10px; }

/* All that stands under a frame: when it was taken, and whether that is long
   enough ago to mean the camera has stopped. It used to share a row with the
   sentence naming the view, held apart from it at either end; with the sentence
   gone there is nothing to hold apart, so the row went with it and the time
   sits under the picture's left edge like every other line in this column. */
.viewer-cam-time {
  padding: 4px 2px 0;
  color: var(--muted);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.viewer-cam-time.stale { color: var(--warn); }

/* The day's text forecast and hourly strips, directly under the plot — ECCC's,
   or the NWS's at the Washington sites, which is why the reference under its
   heading names the service rather than this comment. Its heading carries the
   space above it, the way every other section's does. It reserves its own
   height in app.js while a day step is in flight, being the one section a day
   step re-asks for that has pictures under it. */
.wxpanel {
  max-width: 900px;
  margin: 0 auto;
  cursor: default;
}

/* A strip inside the forecast rather than a section of the view: quieter than
   the headings above it, and with no rule of its own. */
.wxpanel-sub {
  margin: 0;
  padding: 14px 2px 6px;
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
}

.wxperiod {
  display: flex;
  gap: 10px;
  padding: 10px 2px;
}

/* The rule belongs *between* two text periods rather than under every one of
   them: carried on the bottom edge it also drew a line under the last, which
   put a divider between the day's forecast and the Hourly strip that follows —
   and those two are not separate sections. They are the same forecast said
   twice, once in sentences and once hour by hour, and the strip's own heading
   already says where one ends and the other begins. A rule there made the
   hourly read as somebody else's panel bolted underneath. */
.wxperiod + .wxperiod { border-top: 1px solid var(--line); }

.viewer-body .wxperiod-icon {
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  margin: 0;
  border-radius: 0;
  background: transparent;
}

.wxperiod-text { flex: 1 1 auto; min-width: 0; }
.wxperiod-title { color: var(--text); font-size: 14px; font-weight: 700; }
.wxperiod-summary { color: #cbd6e3; font-size: 13px; line-height: 1.35; margin-top: 2px; }
.wxperiod-wind { color: var(--muted); font-size: 12px; line-height: 1.35; margin-top: 3px; }

/* Hourly: a horizontal scroller so a day of hours fits on a phone. */
.wxhours {
  display: flex;
  gap: 6px;
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  padding: 2px 2px 8px;
  scrollbar-width: thin;
}

.wxhour {
  flex: 0 0 auto;
  width: 62px;
  padding: 7px 4px 8px;
  background: #151d27;
  border: 1px solid var(--line);
  border-radius: 9px;
  text-align: center;
}

.wxhour-time { color: var(--muted); font-size: 10.5px; font-weight: 700; }

.viewer-body .wxhour-icon {
  width: 30px;
  height: 30px;
  margin: 3px auto 2px;
  border-radius: 0;
  background: transparent;
}

.wxhour-temp { color: var(--text); font-size: 14px; font-weight: 800; }
.wxhour-wind { color: #a9b8ca; font-size: 10px; font-weight: 600; margin-top: 2px; }

/* The hour's gust, directly under the mean it is read against. Kept in the
   wind's own quiet grey until it reaches one of this app's two gust lines, then
   in the violets the card's edge and the live traces already use: an hour under
   25 km/h is part of the reading rather than a thing to be picked out, and a
   strip where every cell is coloured has marked nothing. The label is spelled
   out for the reason the air row's is - a bare number stacked between a wind and
   a percentage would be read as anything at all. */
.wxhour-gust { font-size: 10px; font-weight: 600; margin-top: 1px; white-space: nowrap; }
.wxhour-gust-tag { color: #6f7f92; font-weight: 600; margin-right: 3px; }
.wxhour-gust-value { color: #a9b8ca; }
.wxhour-gust-value.gust-low { color: var(--gust-low-edge); }
.wxhour-gust-value.gust-high { color: var(--gust-high-edge); }

.wxhour-pop { color: #6f7f92; font-size: 10px; font-weight: 700; margin-top: 1px; }
.wxhour-pop.wet { color: #4bb3fd; }

/* Air quality on the same hour as the temperature and the wind, under a rule
   that separates what the sky is doing from what there is to breathe while it
   does it. Kept to a number rather than a wash, because unlike the tide chart
   this strip is never shrunk to a thumbnail - it is only ever read at the size
   it is written at, and a coloured figure says which hour precisely. */
.wxhour-aqhi {
  margin-top: 4px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
  font-size: 10px;
  font-weight: 700;
  white-space: nowrap;
}

.wxhour-aqhi-tag { color: #6f7f92; font-weight: 600; margin-right: 3px; }
.wxhour-aqhi-value { color: #6f7f92; }
.wxhour-aqhi-value.aqhi-low { color: var(--aqhi-low); }
.wxhour-aqhi-value.aqhi-moderate { color: var(--aqhi-moderate); }
.wxhour-aqhi-value.aqhi-high { color: var(--aqhi-high); }
.wxhour-aqhi-value.aqhi-very-high { color: var(--aqhi-very-high); }

/* The hour cell with the air alone in it, on a day the weather's own hourly has
   run out on. It keeps the width the other cells have, so both strips read at
   the same rhythm - but the rule above the number goes: that line is there to
   divide the sky from the air, and with no sky above it it reads as an
   underline under the hour that nobody asked for. */
.wxhour-air .wxhour-aqhi {
  margin-top: 5px;
  padding-top: 0;
  border-top: 0;
}

.wxair-credit { padding: 0 2px 4px; color: var(--muted); font-size: 11px; }

/* Roomier top bar once there is width for it. */
@media (min-width: 700px) {
  :root { --topbar-h: 82px; }
  #topbar { padding-left: 14px; padding-right: 14px; }
}

/* ------------------------ issued weather alerts --------------------------- */
/* An alert Environment Canada has issued over the region this site stands in:
   a wind warning, a thunderstorm watch, a heat warning, an air quality
   statement. Everything else this map warns about it worked out itself from
   numbers - how bad the index gets, how hard the day is forecast to gust, how
   near the nearest fire is, what the chance of rain reached. This is the one
   mark that is a warning somebody published, for an area, because they judged
   it worth telling people about, and it is the strongest thing said about a
   flying day by anybody.

   So it says so along the card's foot, directly under the weather strip and
   above every other band. The card reads down from the plot to the day's
   conditions and then through what it is warned about, hardest claim first -
   and this belongs at the head of that because it is the day's conditions,
   said by the service whose model drew the picture above it. */
.tile-alert {
  display: none;
  padding: 2px 4px;
  background: rgba(255, 92, 92, 0.2);
  color: #8f0f1c;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile.alerted .tile-alert { display: block; }

/* Chip-sized: dropped along with the name, the way every other band is. What
   survives at that size is the ring, which is why the ring has to be findable
   on its own. */
.tile.tiny .tile-alert { display: none; }

/* And the ring, which is drawn outside every other ring this card can wear.
   That is the whole of what it has to say as a mark: an alert outranks
   anything this app worked out for itself, so it takes the outermost line and
   does not queue for it the way the other warnings queue among themselves -
   there, only one of fire, rain, gusts and smoke is ever drawn, and which one
   is a contest between claims of the same kind. This is not one of them.

   It costs the layout nothing to sit outside them, because it does not sit
   outside them *as well*: the ring around a card is one ring, this colour when
   there is an alert and the other warning's when there is not, and the warning
   that gives up the outside keeps its own colour on the card's own border,
   which lies inside the card. So an alerted card near a fire is orange-bordered
   inside a red ring, and two alerted neighbours still have the same strip of
   map between them that any other pair of warned cards has. A second ring
   drawn around the first would have taken that strip: the rings reach a third
   of the gap the layout keeps between two cards, and two of them stacked is
   two thirds of it from each side of the gap. */
.tile.alerted .tile-card {
  --alert-tone: var(--alert-edge);
  --ring-reach: var(--warn-ring);
}

/* With nothing else warned about, the border under the ring is the card's own
   blue chrome, and a red ring around a blue line is a mount rather than a
   warning - the same lesson the air edge learnt from the pale rim it used to
   wear. So the border joins the ring, one flat tone, exactly as every other
   warned card's does. */
.tile.alerted:not(.fire):not(.rain-high):not(.rain-low):not(.gust-high):not(.gust-low):not(.air) .tile-card {
  border-color: var(--alert-edge);
}

/* And the same weight inwards the other warnings take, into the card's own
   border where it costs the map nothing. Chips keep their hairline: at that
   size the card is nothing but the picture, and a frame there is paid for out
   of the plot. */
.tile.alerted:not(.fire):not(.rain-high):not(.rain-low):not(.gust-high):not(.gust-low):not(.air):not(.tiny) .tile-card {
  border-width: 2px;
}

/* And the alert itself, said in full at the head of the expanded view: what
   was issued, over which forecast region, until when, and the forecaster's own
   opening sentence. The band on the card has room for the name and no more. */
.viewer-alert { border-left-color: var(--alert-edge); }
.viewer-alert b { color: var(--alert-edge); }

.viewer-alert-head { margin-bottom: 2px; }
/* The service's own page, on the end of the head that names it rather than
   under a heading — the banner it sits in is the one block of this view
   deliberately left unheaded. It takes that line's size, so the reference reads
   as part of the sentence it belongs to rather than as something set beside
   it, exactly as each fire's record does further down the banner. */
.viewer-alert-head .viewer-ref { font-size: inherit; }
.viewer-alert-item { margin-top: 4px; }
.viewer-alert-name { font-weight: 600; }
.viewer-alert-text { color: var(--muted); font-size: 11.5px; }

/* ---------------------------- wind gust warning --------------------------- */
/* A day forecast to gust past flying in the hours the site could be flown says
   so along the foot of its own card, third of the four bands: under the issued
   alert and under the fire band where there are any, and above the air band.
   The bands are stacked in the order the warnings outrank each other, and the
   wind ranks there - a fire near the hill is a restriction where a gust figure
   is a forecast of how the air will handle, and how it will handle matters more
   than how pleasant it is to breathe. On the great majority of cards nothing is
   burning and nobody has issued anything, so it still falls directly under the
   weather strip whose wind it is about: it is the same subject as that strip
   read one step harder - that strip prints the day's average wind, and a day
   averaging 18 with 34 in the gusts is not an 18 km/h day. Of everything
   this card works out for itself this is the reading a paraglider's day turns
   on, and until it was added the map said nothing about it without being
   opened.

   In the same two violets the card's edge is drawn in, and washed the way the
   smoke band is washed in its own colour: the band names the figure and the
   edge is what carries across a board of thumbnails, and one warning that came
   out in two colours would read as two. Which step the day reached is carried
   as a tone, the way the AQHI bands set --air-tone above, so the band and the
   edge can only ever be the same violet. */
.tile.gust-high { --gust-tone: var(--gust-high-edge); }
.tile.gust-low { --gust-tone: var(--gust-low-edge); }

.tile-gust {
  display: none;
  padding: 2px 4px;
  background: color-mix(in srgb, var(--gust-tone) 30%, #ffffff);
  color: #16202b;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile.gust-high .tile-gust,
.tile.gust-low .tile-gust { display: block; }

/* Chip-sized: dropped along with the name, for the same reason. */
.tile.tiny .tile-gust { display: none; }

/* --------------------------- air quality warning -------------------------- */
/* A day whose air reaches moderate risk or worse in the hours the site could
   be flown says so along the foot of its own card, last of the four bands -
   the mildest of the warnings, and the one a reader is meant to reach after the
   issued alert, the fire and the wind have already had their say. The wildfire
   zone says where something is burning; this says what is actually in the air
   over this hill, which a fire two valleys away can decide while the nearest
   flame is a hundred kilometres off - and which no windgram will ever
   mention. */
.tile-air {
  display: none;
  padding: 2px 4px;
  background: color-mix(in srgb, var(--air-tone) 30%, #ffffff);
  color: #16202b;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile.air .tile-air { display: block; }

/* Two washes of neighbouring colours stacked read as one band, so every band
   that follows another keeps its own edge. The alert band leads the stack and
   so never needs one: what sits above it is the weather strip or the name,
   neither of which is a wash. */
.tile.alerted .tile-fire,
.tile.alerted .tile-gust,
.tile.alerted .tile-air,
.tile.fire .tile-gust,
.tile.fire .tile-air,
.tile.gust-high .tile-air,
.tile.gust-low .tile-air { border-top: 1px solid rgba(255, 255, 255, 0.6); }

/* Chip-sized: dropped along with the name, for the same reason. */
.tile.tiny .tile-air { display: none; }

/* So the card itself is outlined, which is the part of the warning a chip
   keeps: at that size the band has gone the way of the site's own name, and an
   unmarked chip on a smoky day looks exactly like one on a clean day - which
   is the question a zoomed-out map is being read for. Not the band's own amber,
   because orange is already spoken for here: it is the fire's colour, on the
   zone, on the flame and on the edge of every card near one, so an amber-edged
   card would read as one more of those - and most of all on a card that happens
   to be sitting inside a proximity zone anyway.

   Grey, and not the black this was. The canvas the map opens on is
   rgb(71, 71, 73) over land and rgb(35, 34, 39) over water, which puts black at
   2.3:1 against the ground these cards sit on and 1.3:1 over the inlets a good
   many of these sites fly beside. On a chip, where the outline is the entire
   warning, that is a warning which is simply not there. The grey is 3.5:1
   and 6.0:1 on those two - and it is the colour of the thing being reported
   besides, which is worth something on a mark that has one word's worth of
   meaning and no room for the word.

   The topo layer pays for it, being pale nearly everywhere: black was
   unmissable on it and the grey is found there mostly against the shadow the
   card already casts. That is the right way round for the trade. The dark
   canvas is the layer the map opens on, the topo layer is reached for when the
   question is the ground around a launch rather than which site to open, and
   the outline was never the reading anyway - the band along the card's foot,
   the hover text and the opened site all carry the figure.

   One flat tone across both halves of the mark and out to its edge, with
   nothing else around it. A ring built of two tones is a mount around a card:
   the eye takes the outer tone for the border and leaves the warning as
   whatever is trapped behind it. That was the lesson of the pale rim this edge
   used to wear outside its black, and it holds whichever of the two is on the
   outside. So the grey runs out to where every warning ring now stops: as far
   as the fire ring below and no further, a third of the gap the layout leaves
   between two cards rather than the half it used to be, so two warned
   neighbours keep a strip of map between their rings instead of meeting in the
   middle of it.

   A card already ringed for fire is left alone. An outline is a yes/no mark
   read without looking straight at it, and a second one drawn around the first
   is two rings to tell apart before either has said anything - on a chip,
   where the outline is most of the card, they are one thick smudge. So the
   fire's orange takes the edge by itself. It is the tighter claim of the two:
   something is burning within 5 NM of this hill, where the AQHI is a whole
   airshed's, read off a town that can be 26 km down the sound. And it is the
   mark with nothing to spare, being the one that has to be found against the
   map's own orange - the proximity zone it is about is that colour and usually
   passes right under the card. The air warning is not lost with the grey: at
   every size that carries a band the smoke line is still along the card's
   foot, and the hover text and the opened site still name the figure and the
   hour it arrives at. */
.tile.air:not(.fire):not(.rain-high):not(.rain-low):not(.gust-high):not(.gust-low) .tile-card {
  border-color: var(--air-edge);
  --ring-tone: var(--air-edge);
  --ring-reach: var(--warn-ring);
}

/* And the same weight again inwards. Anything a card spends outside itself
   comes out of the gap the layout keeps between two of them, and that gap is
   map: it is what stops a crowd of warned cards reading as one blot. The
   card's own border sits inside its box, so weight put there costs the layout
   nothing - the same trade the fire ring makes below. Chips keep their
   hairline: at that size the card is nothing but the picture, and a frame
   there is paid for out of the plot. */
.tile.air:not(.fire):not(.rain-high):not(.rain-low):not(.gust-high):not(.gust-low):not(.tiny) .tile-card {
  border-width: 2px;
}

/* ------------------------------- rain risk -------------------------------- */
/* How wet the day looks, in the same edge the other warnings use and ranked
   between them: a fire nearby outranks it, because that is a hazard rather than
   a disappointment, and it outranks the smoke line, because a day with a coin's
   chance of rain is a day the flying is off whatever the air is doing. The
   figure is the one the card already prints in its own strip, so the border
   never disagrees with the words beneath it.

   Only one edge is ever drawn. That was settled when the fire ring took
   precedence over the grey: two rings around one small picture read as a
   frame rather than as two warnings, and the ones that lose the edge are still
   said in the card's hover text and in full inside the opened site. */
.tile.rain-high:not(.fire) .tile-card {
  border-color: var(--rain-high-edge);
  --ring-tone: var(--rain-high-edge);
  --ring-reach: var(--warn-ring);
}

/* The card only ever carries one of the two steps, but the guard against the
   heavier one is written in anyway: a cascade that ranks warnings by which rule
   happens to be further down the file is a cascade that quietly demotes the
   worse warning the day someone reorders it. */
.tile.rain-low:not(.fire):not(.rain-high) .tile-card {
  border-color: var(--rain-low-edge);
  --ring-tone: var(--rain-low-edge);
  --ring-reach: var(--warn-ring);
}

/* The same weight inwards that the other two warnings take, and for the same
   reason: the card's own border is inside its box, so it costs the layout
   nothing, while a chip is nothing but the picture and keeps its hairline. */
.tile.rain-high:not(.fire):not(.tiny) .tile-card,
.tile.rain-low:not(.fire):not(.rain-high):not(.tiny) .tile-card { border-width: 2px; }

/* ------------------------------- wind gusts ------------------------------- */
/* How hard the day gusts, in violet, ranked under the rain and over the smoke.
   Rain outranks it because a wet day is off outright, where a gust figure is a
   forecast of how the air will handle; it outranks the smoke line because wind
   is the thing the wing is actually in, and a day that has blown past flying is
   not improved by clean air.

   The two steps are this app's own gust lines - 35 km/h is where a day has
   blown past flying and 25 where it stops being the easy afternoon, the same
   pair the Squamish Spit's live trace is washed against - so a ring on a card
   and a colour under a trace mean one thing. The figure itself is in the band
   along the card's foot, in this same violet, on every card wearing this
   edge.

   Said in the same two properties every other ring is said in, so an alerted
   card gusting hard keeps the violet on its own border and hands the outer
   ring to the red, the way the fire and the rain do. */
.tile.gust-high:not(.fire):not(.rain-high):not(.rain-low) .tile-card {
  border-color: var(--gust-high-edge);
  --ring-tone: var(--gust-high-edge);
  --ring-reach: var(--warn-ring);
}

/* Guarded against every step above it rather than trusting source order, the
   way the lighter rain rule is: a cascade that ranks warnings by which line
   happens to be further down the file demotes the worse warning the day
   somebody reorders it. */
.tile.gust-low:not(.fire):not(.rain-high):not(.rain-low):not(.gust-high) .tile-card {
  border-color: var(--gust-low-edge);
  --ring-tone: var(--gust-low-edge);
  --ring-reach: var(--warn-ring);
}

/* The same weight inwards the other warnings take, for the same reason: the
   card's own border sits inside its box, so it costs the layout nothing, and a
   chip is nothing but its picture and keeps its hairline. */
.tile.gust-high:not(.fire):not(.rain-high):not(.rain-low):not(.tiny) .tile-card,
.tile.gust-low:not(.fire):not(.rain-high):not(.rain-low):not(.gust-high):not(.tiny) .tile-card {
  border-width: 2px;
}

/* --------------------------- wildfire proximity ---------------------------- */
/* A site inside a fire's 5 NM zone: smoke is a flying consideration, so the
   card is called out rather than just sitting inside an orange circle. It is
   the heaviest edge on the board, because it is the one that has to be found
   against the map's own orange - the zone it is about is drawn in that colour
   and usually passes right under the card - and because on the chips the band
   cannot survive on, this line is the whole warning and colour alone was
   carrying it. */
.tile.fire .tile-card {
  border-color: #ff7a18;
  --ring-tone: rgba(255, 122, 24, 0.85);
  --ring-reach: var(--warn-ring);
  --card-shadow: 0 4px 14px rgba(0, 0, 0, 0.6);
}

/* The rest of that weight goes on the card's own border, which sits inside the
   card's box and so costs the layout nothing - the same trade the air edge
   makes above. Outside the box there is only the gap between two cards to grow
   into, and that gap is worth more as map than as edge: this ring is already
   the heaviest thing on the board without filling it. Chips keep their
   hairline: at that size the card is nothing but the picture, and a frame is
   paid for out of the plot. */
.tile.fire:not(.tiny) .tile-card { border-width: 2px; }

.tile.fire .tile-dot {
  background: #ff7a18;
  box-shadow: 0 0 0 1px rgba(255, 122, 24, 0.6);
}

/* And it says so in words along its own foot, first of the bands this card
   works out for itself - under the issued alert on the rare card carrying one,
   and above the wind and air bands on every card. The orange outline tells you
   something is burning near this site; this names what that means for the day
   without hovering for the tooltip, which on a phone there is no way to do. It
   leads those three because it is the hardest claim among them - a fire alight
   within five miles, where the others are a forecast of how an afternoon will
   handle - and a reader takes the top band for the day's answer whether or not
   the stack was written as a ranking. How
   near and what it is doing is a sentence, and a line this narrow truncated it
   from the right, so that has moved to the bar under the plot in the expanded
   view and this band names the condition alone. It runs the card's full width,
   edge to edge, so it reads as a band rather than a stray line of text, and it
   replaces the flame that used to sit against the name: two marks saying the
   same thing is noise on a card this size, and the name needs its width. */
.tile-fire {
  display: none;
  padding: 2px 4px;
  background: rgba(255, 122, 24, 0.18);
  color: #8a3600;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.2;
  text-align: center;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tile.fire .tile-fire { display: block; }

/* Chip-sized: dropped along with the name, for the same reason. */
.tile.tiny .tile-fire { display: none; }

.tile.fire.displaced .tile-leader { border-top-color: rgba(255, 122, 24, 0.95); }

/* And the fire itself, said in full in the banner at the head of the expanded
   view. The band on the card names the condition and no more, so this is where
   the particulars go - which fire, how far, what it is doing. */
.viewer-fire { border-left-color: #ff7a18; }

.viewer-fire-head { margin-bottom: 2px; }
.viewer-fire-head b { color: #ff7a18; }

.viewer-fire-item { margin-top: 4px; }
.viewer-fire-name { font-weight: 600; }
/* The fire's own record, on the end of its facts line rather than under a
   heading — the banner it sits in is the one block of this view deliberately
   left unheaded. It takes that line's size, so the reference reads as the end
   of the sentence it belongs to rather than as something set beside it. */
.viewer-fire-facts { color: var(--muted); font-size: 11.5px; }
.viewer-fire-facts .viewer-ref { font-size: inherit; }

.firetip { font-size: 12px; line-height: 1.4; }

.leaflet-tooltip {
  background: rgba(17, 24, 33, 0.96);
  border: 1px solid var(--line);
  color: var(--text);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.6);
}

.leaflet-tooltip-top::before { border-top-color: var(--line); }

/* Fire marker: an inline SVG flame at the fire's reported point. It is
   drawn back rather than forward - small, and faded to about half. The flame
   marks where a fire was reported; what that fire means for a flying site is
   said by the zone it throws and by the orange the zone puts on the cards
   inside it, so on a map whose subject is the windgrams the flame only has to
   be findable. At full strength a bad season's worth of them was the loudest
   thing on the board, and the loud mark was the one carrying the least. The
   drop shadow is kept because it fades with the flame and is what stops a
   half-strength orange from dissolving into a dark basemap. Pointing at one
   brings it back to full: that is the gesture that asks about a particular
   fire, and the same one that opens its tooltip. */
.fire-icon {
  background: none;
  border: 0;
  cursor: help;
  opacity: 0.55;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.8));
}

.fire-icon:hover { opacity: 1; }

.fire-icon svg { display: block; }

/* Nothing to report: drop the status strip and give the height to the map. */
#topbar.quiet .statusbar { display: none; }

/* A collapsed pin that would print over a thumbnail keeps only its dot. */
.tile.collapsed.mute .tile-pin { display: none; }

/* ------------------------------- to-do list ------------------------------ */
/* A sheet over the map, hanging from the top bar so the button that opened it
   stays in view and a second tap closes it again. */
.todo {
  position: fixed;
  inset: var(--topbar-h) 0 0 0;   /* clear of the bar, so its button still toggles */
  z-index: 1900;
  display: flex;
  justify-content: center;
  padding: 8px 12px 12px;
  background: rgba(6, 10, 14, 0.55);
}

.todo[hidden] { display: none; }

.todo-sheet {
  display: flex;
  flex-direction: column;
  width: min(420px, 100%);
  max-height: 100%;
  padding: 10px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(10px);
}

.todo-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding-bottom: 8px;
}

/* A thumb target, sized like the ☑ that opens the sheet rather than the smaller
   close the viewer can afford beside a caption: here the nearest thing to miss
   onto is `Lock`, which ends the session. */
.todo-bar .iconbtn {
  width: 42px;
  height: 42px;
  font-size: 26px;
  line-height: 1;
}

.todo-title { flex: 1 1 auto; font-weight: 600; }

.todo-link {
  flex: 0 0 auto;
  padding: 2px 6px;
  color: var(--muted);
  background: transparent;
  border: 0;
  font: inherit;
  font-size: 12px;
  text-decoration: underline;
}

.todo-link[hidden] { display: none; }

/* `Lock` ends the session the password opened, so it is drawn as something to
   press rather than as a word in the bar. The other underlined controls sit
   alone at the head or foot of a view, where a line of text is plainly a
   control; this one sits between a title and a close icon, and among those it
   reads as part of the caption — while a 12px word is the smallest thing in
   the sheet to put a thumb on. It takes the shape of the panel's other buttons
   (`Unlock`, `Add`) so it is recognisably one of them, but stays muted rather
   than accent-toned: those two get work in, and this is only the way back out.
   Smaller than the title, so the bar still reads name-first. */
.todo-lock {
  flex: 0 0 auto;
  height: 32px;
  padding: 0 12px;
  color: var(--muted);
  background: rgba(147, 163, 182, 0.14);
  border: 1px solid var(--line);
  border-radius: 9px;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
}

.todo-lock:hover { color: var(--text); border-color: var(--muted); }

.todo-lock[hidden] { display: none; }

.todo-form {
  display: flex;
  gap: 6px;
  padding-bottom: 8px;
}

.todo-form[hidden] { display: none; }

.todo-input {
  flex: 1 1 auto;
  min-width: 0;
  height: 38px;
  padding: 0 10px;
  color: var(--text);
  background: #1b2430;
  border: 1px solid var(--line);
  border-radius: 9px;
  font: inherit;
}

.todo-input::placeholder { color: var(--muted); }

.todo-go {
  flex: 0 0 auto;
  height: 38px;
  padding: 0 14px;
  color: var(--accent);
  background: rgba(75, 179, 253, 0.16);
  border: 1px solid rgba(75, 179, 253, 0.45);
  border-radius: 9px;
  font: inherit;
  font-weight: 600;
}

.todo-note {
  padding: 0 2px 6px;
  font-size: 12px;
  color: var(--muted);
}

.todo-note[hidden] { display: none; }

.todo-models {
  padding: 0 2px 8px;
  font-size: 11px;
  font-style: italic;
  line-height: 1.4;
  color: var(--muted);
}

.todo-models[hidden] { display: none; }

.todo-list {
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0;
  padding: 0;
  list-style: none;
}

.todo-item {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 9px 4px;
  border-top: 1px solid var(--line);
}

/* The state chip is the item's control as well as its label: one tap moves it
   along queued -> pending -> done, so what it says and what it does are the
   same thing. */
.todo-state {
  flex: 0 0 auto;
  width: 74px;
  padding: 3px 0;
  border-radius: 999px;
  color: var(--muted);
  background: rgba(147, 163, 182, 0.14);
  border: 1px solid var(--line);
  font: inherit;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
}

.todo-item.state-pending .todo-state {
  color: var(--warn);
  background: rgba(240, 160, 90, 0.16);
  border-color: rgba(240, 160, 90, 0.45);
}

.todo-item.state-done .todo-state {
  color: var(--accent);
  background: rgba(75, 179, 253, 0.16);
  border-color: rgba(75, 179, 253, 0.45);
}

.todo-item.state-blocked .todo-state {
  color: var(--warn);
  background: rgba(240, 160, 90, 0.22);
  border-color: var(--warn);
}

/* The agent sets `pending` when a run claims an item, so it reads as activity
   rather than as a state anyone chose. */
.todo-item.state-pending .todo-state { animation: todo-pulse 1.6s ease-in-out infinite; }

@keyframes todo-pulse { 50% { opacity: 0.45; } }

.todo-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
  padding: 0;
  color: inherit;
  background: transparent;
  border: 0;
  font: inherit;
  text-align: left;
}

/* An item a run has written about opens those notes, so it says so. */
.todo-open { cursor: pointer; text-decoration: underline dotted var(--line); }
.todo-open:hover { text-decoration-color: var(--accent); }

/* Finished work stays on the list but stops competing for attention. */
.todo-item.state-done .todo-text { color: var(--muted); text-decoration: line-through; }

.todo-del {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  color: var(--muted);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 7px;
  font-size: 16px;
  line-height: 1;
}

.todo-del:hover { color: var(--warn); border-color: var(--line); }

.todo-foot {
  display: flex;
  justify-content: flex-end;
  padding-top: 6px;
}

/* One item's story, in place of the list: what the run decided and what the
   agent says it did. */
.todo-notes {
  flex: 1 1 auto;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.todo-notes[hidden] { display: none; }

.todo-notes-bar {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
}

.todo-notes-title {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
  color: var(--muted);
}

.todo-notes-body {
  flex: 1 1 auto;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  margin: 0;
  padding: 8px 2px;
  font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  color: var(--text);
}

.todo-foot[hidden], .todo-list[hidden] { display: none; }
