html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  /* STOP iOS INFLATING THE TEXT. Safari on iPhone grows the type in blocks it decides
     are body copy, independently of any size set here, so the phone rendered the top
     bar, the legend and the tab labels noticeably larger than the same CSS does
     anywhere else -- and larger than the layout was measured against. 100% means
     "use the size I asked for". It is not a zoom lock: pinch-zoom still works, and
     the 16px minimum on inputs that stops focus-zoom is a separate rule. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

#map {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 100%;
}

/* Floating control bar across the top. */
#topbar {
  position: absolute;
  top: 10px;
  /* Right-hand side on desktop: the three mode tabs own the top LEFT corner now, so
     these two cannot share a bar with them any more. `left: auto` is load-bearing --
     without it the old `left: 10px` would stretch this across the tabs. */
  left: auto;
  right: 60px;              /* clear of MapLibre's zoom control */
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  background: rgba(255, 255, 255, 0.94);
  padding: 8px 14px;
  border-radius: 8px;
  font: 14px/1.2 -apple-system, Helvetica, Arial, sans-serif;
  color: #333;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}

/* Date and search, side by side. On desktop this is just a segment of the one row; on
   a phone it is the whole first row. */
#bar-row {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* The chip view of #species. Hidden on desktop, where the select is a fine control and
   fits the row; shown instead of it on a phone. One of the two is always hidden, so the
   two never disagree on screen -- and they cannot disagree in VALUE, because the chips
   write to the select rather than keeping their own. */
#species-chips { display: none; }
.chip {
  font: inherit;
  padding: 7px 14px;
  border: 1px solid #c5cdc9;
  border-radius: 999px;
  background: #fff;
  color: #37474f;
  cursor: pointer;
  white-space: nowrap;
}
.chip.on {
  background: #2e7d32;
  border-color: #2e7d32;
  color: #fff;
}

#topbar label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: #555;
}
/* OVAL, like the species chips. The three controls in this bar are now one family:
   same 999px radius, same border colour, same horizontal padding. A square search box
   beside a row of pills read as two different toolbars stacked on each other. */
#topbar input,
#topbar select {
  font: 14px -apple-system, Helvetica, Arial, sans-serif;
  /* An explicit height, not vertical padding. A date input's intrinsic line box is not
     a text input's, so equal padding still gave two different pills -- 30px against 32
     on desktop, and visibly further apart on a phone. border-box plus a stated height
     makes the three controls in this bar one size whatever each one renders inside. */
  height: 32px;
  box-sizing: border-box;
  padding: 0 13px;
  border: 1px solid #c5cdc9;
  border-radius: 999px;
  background: #fff;
  color: #37474f;
}
#topbar input:focus,
#topbar select:focus { outline: 2px solid #2e7d32; outline-offset: 1px; }
/* The native date field puts its calendar button hard against the right edge, which a
   999px corner then clips. A little more room on that side only. */
#topbar input[type="date"] { padding-right: 9px; }

/* THE LEFT-HAND PANEL, shared by Regulations and Conditions.
   The open/closed key used to be its own little box pinned to the bottom-left corner,
   styled separately from the Conditions panel -- a different background, corner, shadow
   and font stack for a thing that answers the SAME question: what do these colours mean.
   Two modes of one app looked like two apps. It is one card now, and the rules live here
   rather than under either id, so the two cannot drift apart again.

   180px is the score ramp measured: seven 22px swatches, each with a 1px border, and 2px
   between them -- 7*24 + 6*2. BOTH rulebooks draw seven, so one number fits both. Sized
   to exactly that, so the widest row sits on one line and nothing is wider than it needs
   to be. If RAMP_STEPS, a swatch, its border or the gap changes, this must change too. */
.panel {
  position: absolute; top: 64px; left: 12px; z-index: 6; max-width: 180px;
  background: rgba(255, 255, 255, .96); border: 1px solid #ccc; border-radius: 6px;
  padding: 14px 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, .18);
  font: 13px/1.5 system-ui, sans-serif;
  color: #333;
}
.panel h2 { margin: 0 0 6px; font-size: 15px; color: #2e7d32; }
.panel p { margin: 0 0 8px; }
.panel .muted { color: #666; }

#legend span {
  display: flex;
  align-items: center;
  margin-bottom: 5px;
}
#legend span:last-child { margin-bottom: 0; }
#legend i {
  display: inline-block;
  width: 16px;
  height: 4px;
  border-radius: 2px;
  margin-right: 8px;
}
/* Low-flow swatch: the open-season green with the yellow casing around it, the
   same pair the map draws. box-shadow rather than a border, so the green core
   keeps the width of the other swatches instead of shrinking inside a frame. */
#legend i.lowflow {
  background: #2e7d32;
  box-shadow: 0 0 0 2px #f2c200;
  margin-left: 2px;
}
#legend a { color: #333; }

/* Search box + results dropdown. */
#search-wrap {
  position: relative;
}
#search {
  width: 170px;
}
#search-results {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  /* ABOVE THE SPECIES CHIPS, which sit below the search box and overlap it once the
     list opens. Being absolutely positioned is normally enough -- a positioned element
     paints over a static sibling -- and in Chromium it is: elementFromPoint inside the
     overlap returns the list.
     iOS Safari disagrees, because the chips carry `-webkit-overflow-scrolling: touch`
     for their horizontal scroll, and that promotes them to their own compositing layer,
     which lifts them over a sibling with no z-index of its own. The result on a phone
     is a list of waters with the chips painted across it.
     A number settles it in both. 5, not 50: this competes with the top bar's own
     children, not with the map panels. */
  z-index: 5;
  width: 240px;
  max-height: 280px;
  overflow-y: auto;
  margin: 0;
  padding: 4px 0;
  list-style: none;
  background: #fff;
  border-radius: 6px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
  font-size: 13px;
}
#search-results li {
  padding: 5px 12px;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
#search-results li:hover,
#search-results li.active {
  background: #e8f5e9;
}
#search-results li.empty {
  color: #999;
  cursor: default;
}

/* Name tooltip shown on hover. */
.hover-tip .maplibregl-popup-content {
  padding: 3px 8px;
  font-size: 12px;
  font-weight: 600;
  color: #1b5e20;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
.hover-tip .maplibregl-popup-tip {
  display: none;
}
/* The touch version of the tip above. MapLibre already sets pointer-events:none on the
   popup container but leaves it AUTO on the content, so the label would be a live click
   target sitting where you just tapped -- the next tap would hit the label instead of
   the map. Nothing in it is clickable, so it takes itself out of hit testing entirely. */
.tap-tip,
.tap-tip .maplibregl-popup-content {
  pointer-events: none;
}

/* Reach popup. */
.maplibregl-popup-content {
  font: 13px/1.45 -apple-system, Helvetica, Arial, sans-serif;
  max-width: 300px;
}
.maplibregl-popup-content h3 {
  margin: 0 0 6px;
  font-size: 15px;
  color: #1b5e20;
}
.maplibregl-popup-content .desc {
  margin: 0 0 6px;
  color: #666;
  font-style: italic;
}
/* The label on the description reads as a heading, not as part of the sentence, so it
   drops the italic the paragraph is set in. */
.maplibregl-popup-content .desc .lbl { font-style: normal; }
.maplibregl-popup-content .status {
  margin: 6px 0 0;
  font-weight: 600;
}

/* ---- Regulations / Conditions modes ------------------------------------ */
/* The topbar's controls belong to Regulations; Conditions hides them and shows
   its own panel. Hiding rather than disabling keeps the bar uncluttered. */
/* The mode tabs. ONE element serves both layouts: a floating segmented control in the
   top-left corner on desktop, a full-width bar along the bottom on a phone (see the
   media query at the foot of this file). Two sets of buttons would be two sets of
   listeners and two places to forget a mode. */
#tabbar {
  position: absolute;
  top: 10px; left: 10px;
  z-index: 11;
  display: flex;
  font: 14px/1.2 -apple-system, Helvetica, Arial, sans-serif;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  border-radius: 4px;
}
.mode {
  font: inherit; padding: 7px 14px; cursor: pointer;
  border: 1px solid #bbb; background: #fff; color: #444;
  /* Icon beside the label on desktop, where this bar is a small segmented control and
     stacking them would double its height. The phone bar stacks instead -- see the
     media query. */
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
/* currentColor everywhere inside, so the icon follows the button's own state rather
   than needing a rule per state. */
.mode-ico { width: 18px; height: 18px; flex: none; display: block; }
.mode:first-child { border-radius: 4px 0 0 4px; }
.mode:last-child { border-radius: 0 4px 4px 0; border-left: none; }
.mode.on { background: #2e7d32; border-color: #2e7d32; color: #fff; }
.mode:hover:not(.on) { background: #f2f2f2; }
/* `.regs-only` marks what belongs to Regulations alone -- the water search and the
   open/closed legend. Date and species are shared with Conditions and stay. The
   selectors carry an id because `#topbar`/`#legend` rules would otherwise outrank a
   plain class on specificity. */
body.conditions #topbar .regs-only,
body.conditions #legend.regs-only,
body.conditions .regs-only { display: none !important; }

/* #cond-panel's own card rules moved to .panel above, which it now shares with the
   Regulations key. Everything specific to the Conditions panel still lives below. */

/* Gauge popups (Conditions). MapLibre's popup is unstyled by default. */
.maplibregl-popup-content { font: 13px/1.5 system-ui, sans-serif; padding: 12px 14px; }
.maplibregl-popup-content h3 { margin: 0 0 6px; font-size: 14px; color: #1565c0; }
.maplibregl-popup-content p { margin: 0 0 6px; }
.maplibregl-popup-content .muted { color: #666; font-size: 12px; }

/* The rulebook's two columns, kept apart in the popup the way they are on the page.
   A window with no bag limit prints only the season block. */
.maplibregl-popup-content .reg-win + .reg-win {
  border-top: 1px solid #e3e3e3;
  margin-top: 8px;
  padding-top: 8px;
}
.maplibregl-popup-content .reg-season,
.maplibregl-popup-content .reg-bag { margin: 0 0 6px; font-size: 12.5px; }
.maplibregl-popup-content .reg-bag { color: #444; }
.maplibregl-popup-content .lbl {
  display: block;
  font-size: 10px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #777;
  margin-bottom: 1px;
}

/* The low-flow note in a river popup: same yellow the map draws around the
   line, so the popup and the map say the same thing. */
.maplibregl-popup-content .lowflow {
  border-left: 3px solid #f2c200;
  background: #fffbe8;
  padding: 6px 8px;
  font-size: 12px;
}

/* The disclaimer that closes every popup stating a RULE (§7.40, §7.50, §7.00).
   RED, because it is a warning and should read as one -- #c62828, the same red the
   map already uses for "closed all year", rather than a second red nobody can place.
   Still 11px and separated by a rule, so it is a footer on every popup without
   competing with the season somebody opened the popup to read.
   The LINK is ordinary link blue, not the red and not the app's green: it is the one
   actionable thing in the sentence, and it should look like the thing people already
   know how to click.

   #1976d2 rather than a lighter blue, because this is 11px and small text needs 4.5:1
   against white to stay readable. Measured on white: #1976d2 is 4.60:1 and passes;
   #2196f3, which is what "light blue" usually means, is 3.12:1 and does not. This is
   the lightest blue that still reads -- and a disclaimer nobody can read is worse than
   no disclaimer. */
.maplibregl-popup-content .disclaimer {
  margin: 10px 0 0;
  padding-top: 7px;
  border-top: 1px solid #e3e8e5;
  font-size: 11px;
  line-height: 1.45;
  color: #c62828;
}
.maplibregl-popup-content .disclaimer a { color: #1976d2; }

/* The menu's own Add-to-Home-Screen entry. A button, not a toggle, so it does not
   wear a checkbox for something that is not a layer; the rule above it says it belongs
   to the app rather than to the map. */
#install-now {
  display: block;
  width: 100%;
  margin-top: 6px;
  padding: 7px 2px;
  border: 0;
  border-top: 1px solid #e3e8e5;
  background: none;
  color: #1b5e20;
  font: 600 13px system-ui, sans-serif;
  text-align: left;
  cursor: pointer;
}
#install-now[hidden] { display: none; }

/* The Add-to-Home-Screen hint. Bottom of the map, above the tab bar on a phone and
   clear of the corner controls on desktop; dark so it reads as the app talking rather
   than as part of the map, and narrow enough that it never becomes a wall. It appears
   once and remembers being dismissed -- see wireInstallTip(). */
#install-tip {
  position: fixed;
  /* left+right+auto margins, NOT left:50% with a transform: a fixed element with no
     width shrink-wraps its content, and the flex children then squeezed the sentence
     into a six-line column about 180px wide. Pinning both edges gives it the width to
     be one line on a phone, and the max-width still centres it on a desktop. */
  left: 12px;
  right: 12px;
  margin: 0 auto;
  bottom: calc(var(--tabbar-h, 12px) + 12px);
  z-index: 12;                 /* over the sheet (10) and the search results (11) */
  display: flex;
  align-items: center;
  gap: 10px;
  max-width: 520px;
  padding: 10px 8px 10px 14px;
  border-radius: 10px;
  background: #1b2b26;
  color: #eaf1ee;
  font: 13px/1.35 system-ui, sans-serif;
  box-shadow: 0 4px 16px rgba(0, 0, 0, .3);
}
#install-tip[hidden] { display: none; }
/* The sentence takes the slack; the button and the dismiss keep their size. min-width
   lets it actually shrink rather than forcing the bar wider than the screen. */
#install-tip span { flex: 1 1 auto; min-width: 0; }
#install-tip b { color: #fff; }
#install-tip .it-go {
  flex: 0 0 auto;
  border: 0;
  border-radius: 999px;
  padding: 7px 14px;
  background: #2e7d32;
  color: #fff;
  font: 600 13px system-ui, sans-serif;
  cursor: pointer;
}
#install-tip .it-x {
  flex: 0 0 auto;
  width: 28px; height: 28px;
  border: 0; background: none;
  color: #9fb3ab;
  font-size: 19px; line-height: 1;
  cursor: pointer;
}

/* Key for the gauge dots in the Conditions panel. The #legend swatches are 16x4 bars
   because they stand for LINES; a gauge is a circle, so it needs its own round swatch. */
/* Sized and coloured to match .res-key exactly. It used to inherit the panel's 13px
   black, so the temperature key rendered a size larger and darker than every other
   legend beside it -- which read as emphasis nobody meant. */
.gauge-key { list-style: none; margin: 8px 0 0; padding: 0;
             font-size: 12px; color: #455a64; }
.gauge-key li { display: flex; align-items: center; margin: 4px 0; }
.gauge-key i.dot {
  display: inline-block;
  width: 11px;
  height: 11px;
  border-radius: 50%;
  margin-right: 8px;
  border: 1.5px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
  flex: none;
}
.gauge-key .muted { margin-left: 5px; }
/* The temperature range, given a fixed width so the labels beside it line up into a
   column and the key reads as a scale rather than a list. Centred within that column:
   the ranges are different lengths ("<50°F" against "65-68°F"), and ragged on both
   sides reads as a column of its own rather than as text shoved against the labels. */
.gauge-key .rng {
  flex: 0 0 auto; min-width: 58px; margin-right: 8px;
  font-variant-numeric: tabular-nums; font-weight: 600; text-align: center;
}

/* Temperature bands stand for LINES on the map, so their swatch is a bar like the
   regulation legend's -- not the round dot the gauge key uses for point stations. */
.gauge-key i.bar {
  display: inline-block;
  width: 16px;
  height: 4px;
  border-radius: 2px;
  margin-right: 8px;
  flex: none;
}
.cond-h { font-size: 13px; margin: 14px 0 0; }

/* ---- Research tab -------------------------------------------------------------
   Not a map tab: the panel covers the map entirely rather than floating over it, so
   the map's own controls (date, species, search, legend, zoom readout) are hidden and
   nothing shows through at the edges. */
body.research .map-only,
body.research #legend,
body.research #search-wrap,
body.research .regs-only { display: none !important; }

#research-panel {
  position: absolute;
  top: 52px; left: 0; right: 0; bottom: 0;
  z-index: 8;
  overflow-y: auto;
  background: #f7f8f7;
  font: 13px/1.5 system-ui, sans-serif;
  color: #263238;
  padding: 18px 22px 40px;
}
#res-head { max-width: 980px; }
#research-panel h2 { margin: 0 0 4px; font-size: 18px; color: #2e7d32; }
#res-intro { margin: 0 0 14px; max-width: 620px; }

/* Section headings inside the Conditions panel: LAYERS (pick one) and LEGEND (read
   it). Quiet small-caps rather than another green heading -- they are signposts between
   two groups, not titles competing with the panel's own. */
.panel-sec {
  margin: 14px 0 7px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: #5b6b65;
}
#cond-panel .panel-sec:first-of-type { margin-top: 10px; }
/* The rule under LEGEND separates the key from the buttons without a heavy box. */
#cond-legend { border-top: 1px solid #e3e8e6; padding-top: 10px; }

.res-picks { display: flex; flex-direction: column; gap: 7px; margin-bottom: 14px; }
.res-row { display: flex; align-items: center; gap: 10px; }
.res-label {
  width: 62px; flex: none; font-size: 11px; text-transform: uppercase;
  letter-spacing: .04em; color: #5b6b65;
}
.res-group { display: flex; flex-wrap: wrap; gap: 6px; }
.res-group button {
  font: 13px system-ui, sans-serif;
  padding: 5px 12px; border: 1px solid #c5cdc9; border-radius: 6px;
  background: #fff; color: #37474f; cursor: pointer;
}
/* The Conditions layer menu runs as a COLUMN -- one button per row, each the full
   width of the panel. Wrapped in rows they broke unevenly as the labels changed length
   ("Gauges" next to "Seasonal conditions"), so the menu reflowed whenever the rulebook
   changed. A column is the same shape whatever the labels say.
   Scoped to #cond-layers: the Research tab's .res-group rows stay as they are. */
#cond-layers { flex-direction: column; align-items: stretch; gap: 5px; }
#cond-layers button { text-align: left; }

.res-group button:hover { border-color: #8fa39b; }
.res-group button.on {
  background: #2e7d32; border-color: #2e7d32; color: #fff; font-weight: 600;
}

/* The key is built from the same levels the cells are painted from. */
.res-key { display: flex; flex-wrap: wrap; gap: 14px; align-items: center;
           margin: 0 0 12px; font-size: 12px; color: #455a64; }
.res-key span { display: flex; align-items: center; gap: 6px; }
.res-key i { width: 14px; height: 14px; border-radius: 3px; display: inline-block;
             border: 1px solid rgba(0,0,0,.12); }

/* STACKED legends: river flow, seasonal conditions and fishery quality. One band per
   row, so the four §7.40 keys are read the same way instead of three running as a
   wrapped line and the temperature one as a column.
   The SCORE key is deliberately left unstacked -- it is a continuous ramp from 0 to 4,
   and a gradient broken into rows stops being a gradient. */
.res-key.stacked { flex-direction: column; align-items: flex-start; gap: 5px; }
/* These bands stand for LINES on the map, so the swatch is the temperature key's 16x4
   bar rather than the flat colour chip .res-key gives a square. */
.res-key.stacked i.bar {
  width: 16px; height: 4px; border-radius: 2px; border: 0; flex: none;
}
/* The trailing "N reaches for Sep" count is a footnote, not a band. */
.res-key.stacked > span.muted { margin-top: 3px; }

/* The table scrolls inside its own box -- twelve month columns plus four detail columns
   is wider than a phone, and the page itself must never scroll sideways. */
#res-body { overflow-x: auto; background: #fff; border: 1px solid #dfe4e1;
            border-radius: 8px; }
#res-table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
#res-table th, #res-table td { padding: 6px 8px; text-align: left; white-space: nowrap; }
#res-table thead th {
  position: sticky; top: 0; z-index: 2;
  background: #eef2f0; border-bottom: 1px solid #cfd8d3;
  font-size: 11px; text-transform: uppercase; letter-spacing: .03em; color: #49605a;
}
#res-table tbody tr:nth-child(even) { background: #fbfcfb; }
#res-table tbody tr:hover { background: #f0f6f2; }
/* THE FIRST COLUMN STAYS PUT while the twelve month columns scroll under it. Scrolled
   right, the table used to show nothing but colour: the row you were reading had lost
   its river name off the left edge.

   A sticky cell is painted OVER the cells sliding beneath it, so it needs an opaque
   background of its own -- which is why the row stripe and the hover colour are
   repeated here rather than inherited from the row. The header's first cell is sticky
   in both directions at once (`top` comes from the thead rule above) and sits above
   both, so the corner never has a month name sliding through it. */
#res-table thead th:first-child {
  position: sticky; left: 0; z-index: 3; background: #eef2f0;
}
#res-table tbody td:first-child {
  position: sticky; left: 0; z-index: 1; background: #fff;
}
#res-table tbody tr:nth-child(even) td:first-child { background: #fbfcfb; }
#res-table tbody tr:hover td:first-child { background: #f0f6f2; }
/* The seam, so a month cell passing underneath does not read as part of the name. */
#res-table th:first-child, #res-table td:first-child { box-shadow: 1px 0 0 #cfd8d3; }
/* HALF THE WIDTH IT TOOK. The run tables' river column was sized by its longest name --
   "Sacramento (Redding-Red Bluff)", 216px -- and since the column is now frozen in
   place, that was 216px of a 362px phone screen the months never got back.

   Narrowed by letting a long name WRAP rather than by cutting it off: a truncated river
   name on a phone has no hover to recover it from. The handful of long names take two
   lines; most are one word.

   Not the hatch tables (`.hatch`): their first column is a hatch name sitting beside two
   prose columns, and it is already sized against those. */
#res-table:not(.hatch) th:first-child,
#res-table:not(.hatch) td.river {
  white-space: normal;
  width: 108px;
  min-width: 108px;
  max-width: 108px;
}
/* The hatch table used to carry two prose columns the run tables do not: life stages
   and a list of fly patterns running to 200 characters. Between them they left a phone
   almost no room for the months.

   LIFE STAGES lost its column and now rides under the hatch name, in the same cell --
   it is two or three words, and a column of "Nymph, Dry" is not worth 170px. */
#res-table.hatch th:first-child,
#res-table.hatch td.river {
  white-space: normal;
  width: 124px;
  min-width: 124px;
  max-width: 124px;
}
.hstage-sub {
  display: block;
  margin-top: 1px;
  font-size: 11px;
  font-weight: 400;      /* td.river is 600; the stages are a caption, not a heading */
  color: #6b7b75;
}
/* FLY PATTERNS are clamped to two lines and open on a tap. The clamp is on the span
   inside, never the cell: -webkit-line-clamp needs display:-webkit-box, and a <td> set
   to that stops being a table cell and collapses the row. */
#res-table.hatch td.hflies {
  white-space: normal;
  min-width: 150px;
  max-width: 210px;
  color: #4a5a54;
  cursor: pointer;
}
#res-table.hatch td.hflies .clamp {
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
#res-table.hatch td.hflies.open .clamp { -webkit-line-clamp: unset; }
/* The twelve-month strip in the hatch popup: a row of blocks with the month initials
   under them, the selected month ringed. */
.gs-strip-l { display: flex; gap: 2px; margin: 2px 0 8px; }
.gs-strip-l span { flex: 1; text-align: center; font-size: 9px; color: #8a9a94; }
.gs-strip-l span.on { color: #1b2b26; font-weight: 700; }
#res-table.hatch td.river a { color: inherit; text-decoration: none;
                              border-bottom: 1px dotted #9bb0a8; }
#res-table.hatch td.river a:hover { border-bottom-style: solid; }
#res-table td.river { font-weight: 600; }
#res-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
/* Bare colour, so the columns need only enough width to read as blocks -- narrower than
   when they held two letters, which fits the whole fishing year on a smaller screen.

   THE SAME BOXES THE TROUT POPUP DRAWS (.gs-c): separated rounded blocks rather than a
   continuous band of colour, so each month is its own mark and an empty one is a grey
   box rather than an absence. The colour is on an inner <i> because this table is
   border-collapse:collapse, where border-radius on a <td> does not render -- the cell
   keeps the column width and contributes the gap as padding. */
#res-table .mcell {
  text-align: center; width: 34px; min-width: 34px;
  font-size: 11px;
  padding: 3px 5px;                 /* the horizontal pad is the gap between boxes */
}
#res-table .mcell > i {
  display: block;
  height: 21px;                     /* with a 24px box width this reads as a square */
  border-radius: 4px;
  background: #eceff1;              /* nothing that month; an inline style overrides it */
}
/* This month, so the answer to "right now" needs no counting along the row. A ring
   around each box, as in the popup, rather than the rails the full-bleed cells used:
   with gaps between the boxes the rails had nothing to run along. The white ring first
   keeps the dark one off the colour it surrounds.
   THE HEADER IS LEFT ON ITS RAILS deliberately -- the month row at the top is unchanged. */
#res-table td.now > i { box-shadow: 0 0 0 1.5px #fff, 0 0 0 3px #37474f; }
#res-table th.now { box-shadow: inset 2px 0 0 #37474f, inset -2px 0 0 #37474f; }
#res-table th.now { color: #1b2b26; font-weight: 700; }
.badge { display: inline-block; padding: 1px 7px; border-radius: 10px;
         font-size: 11px; color: #fff; font-weight: 600; }
.res-note { padding: 14px; color: #5b6b65; }
.res-count { font-size: 12px; color: #5b6b65; margin: 0 0 8px; }

/* Concern: a colour dot with the word beside it. The dot carries the severity, the word
   carries the meaning -- so it never gets mistaken for the green quality badge next to
   it, which runs on the opposite scale. */
#res-table .cdot-wrap {
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
}
#res-table i.cdot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .18);
}

/* ---- Conditions score popup: four boxes in a square --------------------------
   One box per input, so a single averaged figure is never shown without the numbers
   behind it. Flow is the fourth box and is deliberately score-less for now. */
.score-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin: 8px 0 6px;
}
.score-grid .sb {
  border: 1px solid #dfe4e1;
  border-radius: 7px;
  padding: 7px 9px;
  background: #fbfcfb;
  min-width: 0;
}
.sb-label {
  font-size: 10px; text-transform: uppercase; letter-spacing: .04em;
  color: #6b7b75; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sb-value { font-size: 15px; font-weight: 700; color: #22312c; margin: 1px 0 2px; }
.sb-unit { font-size: 11px; font-weight: 600; color: #6b7b75; }
.sb-foot { font-size: 11px; color: #49605a; display: flex; align-items: center; gap: 5px; }
.sb-dot {
  width: 9px; height: 9px; border-radius: 50%; flex: none;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .18);
}
.sb-none { color: #90a4ae; font-style: italic; }
.sb-head { margin: 2px 0 0; }

/* ---- Gauge popup: now / range tabs / chart --------------------------------- */
.gp-now { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin: 2px 0 8px; }
.gp-cell { background: #f4f6f5; border-radius: 6px; padding: 5px 7px; min-width: 0; }
.gp-cell .gp-k { font-size: 11px; color: #666; }
.gp-cell .gp-v { font-size: 15px; font-weight: 600; color: #222; white-space: nowrap; }
.gp-cell .gp-u { font-size: 11px; font-weight: 400; color: #666; }
.gp-cell .gp-none { font-size: 12px; font-weight: 400; color: #999; }
.gp-cell .gp-sub { font-size: 11px; color: #666; }
.gp-tabs { display: flex; background: #eef0ef; border-radius: 7px; padding: 2px; margin-bottom: 6px; }
.gp-tabs button {
  flex: 1; border: 0; background: transparent; padding: 4px 0; border-radius: 5px;
  font: inherit; font-size: 12px; color: #777; cursor: pointer;
}
.gp-tabs button.on { background: #fff; color: #222; font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.14); }
.gp-chart { min-height: 190px; }
.gp-chart .gp-wait { padding: 70px 0; text-align: center; }
.gp-svg { width: 100%; height: auto; display: block; }
.gp-grid   { stroke: #e7e7e7; stroke-width: 1; }
.gp-grid-x { stroke: #f1f1f1; stroke-width: 1; }
.gp-ax     { font: 10px system-ui, sans-serif; fill: #777; }
.gp-ax-t   { fill: #ef6c00; }
.gp-flow   { fill: none; stroke: #1565c0; stroke-width: 2;   stroke-linejoin: round; }
.gp-temp   { fill: none; stroke: #ef6c00; stroke-width: 1.5; stroke-linejoin: round; }
.gp-med    { fill: none; stroke: #616161; stroke-width: 1.5; }
.gp-legend { display: flex; flex-wrap: wrap; gap: 12px; font-size: 11px; color: #555; margin-top: 2px; }
.gp-legend i { display: inline-block; width: 14px; height: 3px; border-radius: 2px; margin-right: 5px; vertical-align: middle; }
.gp-foot { margin-top: 6px; }

/* Reach popups: one reading, coloured by its band, and the bands spelled out. */
.gp-now.one { grid-template-columns: 1fr; }
.gp-band-l { font-size: 13px; font-weight: 600; margin-left: 8px; }
.gp-ranges { margin: 8px 0 2px; font-size: 11.5px; }
.gp-r { display: flex; align-items: center; gap: 7px; padding: 1px 0; color: #666; }
.gp-r i { width: 12px; height: 10px; border-radius: 2px; flex: 0 0 auto; }
.gp-r .gp-r-v { min-width: 84px; font-variant-numeric: tabular-nums; }
.gp-r.on { color: #111; font-weight: 600; }

/* Reading on the left, the ranges that judge it on the right. */
.gp-now.two { grid-template-columns: auto 1fr; align-items: stretch; }
.gp-now.two .gp-cell { min-width: 96px; }
.gp-now.two .gp-band-l { margin-left: 0; font-size: 13px; }
.gp-now.two .gp-ranges { margin: 0; padding: 3px 0 0 4px; align-self: center; }
.gp-now.two .gp-r .gp-r-v { min-width: 66px; }
/* A range row must never break ("Stop / fishing"): the popup sizes to its content, so
   letting these wrap let the reading's note steal the width. Instead the note wraps
   inside its own column, and the popup widens to fit the ranges on one line each. */
.gp-now.two .gp-r { white-space: nowrap; }
.gp-now.two .gp-cell { max-width: 128px; }

/* ---- Popup views: back arrow, clickable score boxes, species blocks ----------- */
.gp-back {
  display: inline-block; border: 0; background: none; padding: 0 0 4px; margin: 0;
  font: inherit; font-size: 12px; color: #1565c0; cursor: pointer;
}
.gp-back:hover { text-decoration: underline; }
.score-grid .sb.go { cursor: pointer; transition: border-color .12s, background .12s; }
.score-grid .sb.go:hover, .score-grid .sb.go:focus-visible {
  border-color: #90a4ae; background: #f6f9f8; outline: none;
}
/* The chevron sits in the box's corner, OUT of the label's flow: inside it, it took
   width from a label that is already nowrap + ellipsis, and cut "Seasonal
   conditions" to "SEASONAL CONDITIO...". */
.score-grid .sb.go { position: relative; }
.score-grid .sb.go .sb-label {
  padding-right: 12px;
  /* "Seasonal conditions" is wider than half the popup at this size, with or
     without the chevron -- so wrap it rather than cut it. The grid row stretches
     to its tallest box, so the box beside it stays level. */
  white-space: normal; overflow: visible; text-overflow: clip;
}
.sb-more { position: absolute; top: 5px; right: 7px; color: #90a4ae; font-size: 14px; line-height: 1; }
.gs-sp { padding: 6px 0 4px; border-top: 1px solid #eceff1; }
.gs-sp:first-of-type { border-top: 0; padding-top: 0; }
.gs-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.gs-q { font-size: 11px; color: #666; }
.gs-now { margin: 2px 0 4px; font-size: 12px; }
.gs-strip { display: flex; gap: 2px; }
.gs-c { flex: 1; height: 14px; border-radius: 2px; }
.gs-c.now { box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px #37474f; }
.gs-ml { margin-top: 1px; }
.gs-m { flex: 1; text-align: center; font-size: 9px; color: #999; }
.gs-m.now { color: #222; font-weight: 700; }
.gs-key { margin-top: 6px; }
.gs-key i { width: 10px; height: 10px; }
/* The hatch grid in the trout popup: one row per hatch, the name on the left and the
   twelve months to the right of it, all rows sharing one column rhythm. */
.ha-grid { margin-top: 2px; }
.ha-row { display: flex; align-items: center; gap: 6px; margin-bottom: 2px; }
.ha-n { flex: 0 0 92px; font-size: 11px; line-height: 1.2; color: #1b2b26;
        overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ha-row .gs-strip { flex: 1; }
.ha-row .gs-c { height: 11px; }
.cond-h { margin: 0 0 4px; font-size: 13px; }
/* Fly patterns for the hatches at peak, under the legend in the trout popup. */
.ha-flies { margin-top: 8px; border-top: 1px solid #eceff1; padding-top: 6px; }
.ha-fh { font-size: 11px; font-weight: 700; color: #1b2b26; margin-bottom: 3px; }
.ha-f { font-size: 11px; line-height: 1.35; margin-bottom: 2px; color: #4a5a54; }
.ha-f b { color: #1b2b26; }
/* ONE BOX PER PEAKING HATCH, full width. Borrows the score boxes' shell -- same
   border, radius, padding and tint -- so the popup reads as one family of cards, but
   one column rather than the 2x2 grid: these are a list of hatches, not a set of
   figures to compare side by side.

   Collapsed, the patterns are ONE LINE with a real ellipsis. Opened, the line wraps and
   shows every pattern for that hatch alone. `.go` and the chevron are added by
   wirePeakFlies() only when the line is genuinely cut off, so a hatch with two flies
   has no chevron and nothing to click. */
.ha-flies .hb {
  position: relative;
  margin-top: 5px;
  padding: 7px 9px;
  border: 1px solid #dfe4e1;
  border-radius: 7px;
  background: #fbfcfb;
}
.ha-flies .hb .sb-label { padding-right: 12px; white-space: normal; }
.hb-list {
  font-size: 11px;
  line-height: 1.35;
  color: #4a5a54;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ha-flies .hb.open .hb-list {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
}
.ha-flies .hb.go { cursor: pointer; transition: border-color .12s, background .12s; }
.ha-flies .hb.go:hover,
.ha-flies .hb.go:focus-visible { border-color: #90a4ae; background: #f6f9f8; outline: none; }
.ha-flies .hb .sb-more { transition: transform .12s; }
.ha-flies .hb.open .sb-more { transform: rotate(90deg); }

/* Zoom readout, bottom-left beside the scale bar. Tabular monospace figures so the
   number does not jiggle the box wider as it ticks through 9.9 -> 10.0 while panning. */
.zoom-readout {
  padding: 3px 8px;
  font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-variant-numeric: tabular-nums;
  color: #334;
  letter-spacing: 0.02em;
  background: rgba(255, 255, 255, 0.92);
  cursor: default;
  user-select: none;
}

/* Score ramp: swatches on one row, the two end labels on the next. The wrapper takes
   the width of the bars, and .ramp-ends then spreads across exactly that width -- so
   "0 worst" and "4 best" sit under the ENDS of the ramp instead of running inline with
   it and wrapping into what looks like a second scale.

   Every selector is scoped under .res-key ON PURPOSE. `.res-key span` already sets
   display:flex on every span inside the key, and it is the more specific selector, so
   an unscoped `.ramp` loses to it -- which laid the bars and the labels out side by
   side instead of stacked. */
.res-key .ramp { display: flex; flex-direction: column; align-items: stretch; }
.res-key .ramp-bars { display: flex; gap: 2px; align-items: center; }
/* SQUARE: the same 22px the JS sets for the width. A score is a point on a scale, not
   a line on the map, so a square swatch says "step" where the flat bars elsewhere say
   "this is what a reach looks like". */
.res-key .ramp-bars i.bar { height: 22px; border-radius: 3px;
                            border: 1px solid rgba(0,0,0,.12); }
.res-key .ramp-ends { display: flex; justify-content: space-between; gap: 0;
                      margin-top: 5px; }
.res-key .ramp-ends > span { gap: 4px; }

/* The NOAA history button. Behind a press because the series is 250 KB uncompressed
   -- see noaaSeries in app.js -- so the cost is stated on the button rather than
   spent on a phone for a chart nobody asked to see. */
.gp-hist {
  font: 12px system-ui, sans-serif;
  padding: 5px 10px; margin-top: 6px;
  border: 1px solid #c5cdc9; border-radius: 6px;
  background: #fff; color: #37474f; cursor: pointer; width: 100%;
}
.gp-hist:hover { border-color: #8fa39b; }

/* The public-lands checkbox. Deliberately not styled like the layer buttons above it:
   those are a pick-one set, this is an overlay that stays on across them, and making
   it look like a sixth button would invite the wrong expectation. */
.lands-toggle {
  display: flex; align-items: center; gap: 7px;
  margin-top: 8px; font-size: 12.5px; color: #37474f; cursor: pointer;
  user-select: none;
}
.lands-toggle input { margin: 0; cursor: pointer; accent-color: #2e7d32; }

/* A POPUP THAT IS TOO TALL SCROLLS ITSELF. Some regulations are long -- the Klamath
   carries several season windows and a bag limit for each -- and a popup sized to its
   content ran off the top and bottom of the screen with no way to read the rest.

   The scrolling happens in an inner `.popup-scroll` box, NOT on the content itself, and
   that is what keeps the close button still. MapLibre appends the button as the last
   child of the content, so:
     - left in the flow it lands at the BOTTOM of a long popup;
     - absolute inside a SCROLLING content box it scrolls away with the text;
     - absolute against `.maplibregl-popup` it is pinned to the wrapper, which includes
       the tip -- so it hangs off the edge of the white box, outside the popup.
   Wrapping the text in its own scroller fixes all three: the content stays still and
   stays the positioning context, so the button sits in the corner of the white box,
   inside it, and the text scrolls underneath. The wrapper is made in app.js, because
   MapLibre owns this markup and rebuilds it whenever the popup's HTML is replaced.

   The height cap is set in JS too, by centrePopup(), because the room available depends
   on what chrome is covering the map at the time.

   `overscroll-behavior: contain` stops a flick past the end handing the gesture on to
   whatever is underneath. The map is kept out of it in app.js, by stopping wheel and
   touch events from reaching the handlers it has on the container. */
.maplibregl-popup-content { position: relative; }
/* ABOVE THE TWO LEFT-HAND PANELS. A popup is a child of the map container, and that
   container has no z-index of its own, so the panels -- z-index 6, and siblings of the
   map rather than children of it -- painted straight over any popup they overlapped.
   What they covered was the BOTTOM of the popup, which is the one part scrolling inside
   the popup cannot bring into view. 7 is above the panels and still below the top bar
   (10) and the search results (11), which a popup never reaches: centrePopup() caps it
   to the room those leave. */
.maplibregl-popup { z-index: 7; }
.popup-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;   /* momentum scrolling on iOS */
}
/* No disc, no border -- just the glyph, in the corner of the content box where the
   padding already leaves room for it. */
.maplibregl-popup-close-button {
  position: absolute;
  top: 1px;
  right: 3px;
  z-index: 3;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  background: none;
  color: #90a4ae;
  font-size: 18px;
  line-height: 20px;
  cursor: pointer;
}
.maplibregl-popup-close-button:hover { color: #37474f; background: none; }

/* ---- fishing reports popup --------------------------------------------- */
/* Somebody else's report, so the title leads -- that is what tells you whether it is
   worth the click -- and who wrote it and when sit under it in a quieter second line.
   Only the title is a link, so a mis-tap on the date does not open a new tab. */
.report-list { list-style: none; margin: 2px 0 8px; padding: 0; }
.report-list li { padding: 6px 0; border-bottom: 1px solid #eceff1; }
.report-list li:last-child { border-bottom: 0; }
.report-list a {
  display: block;
  color: #1b5e20;
  font-weight: 600;
  text-decoration: none;
  line-height: 1.35;
}
.report-list a:hover { text-decoration: underline; }
.report-list .rmeta {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  color: #78909c;
}

/* ---- Layers control ---------------------------------------------------- */
/* Right-hand side, under MapLibre's zoom buttons, where a map's layer switcher is
   normally found. It holds the OVERLAYS -- things that stay on across the mode and
   across the Conditions layer picker -- so it is deliberately not part of either
   panel on the left. */
#layers-ctl {
  position: absolute;
  top: 120px;             /* clear of the three stacked zoom/compass buttons */
  right: 10px;
  z-index: 10;
  font: 13px/1.5 system-ui, sans-serif;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}
/* Research replaces the map, so there is nothing to put a layer over. */
body.research #layers-ctl { display: none; }

/* A circle with the icon in it, the shape a map's floating controls take. The word
   "Layers" is still in the markup for a screen reader and still in the title attribute
   for a hover tooltip -- it is only taken off the SCREEN, not out of the page. */
#layers-btn {
  font: inherit;
  display: flex; align-items: center; justify-content: center;
  width: 40px; height: 40px; padding: 0;
  background: rgba(255, 255, 255, .96);
  border: 1px solid #ccc;
  border-radius: 50%;
  color: #37474f;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .18);
}
#layers-btn svg { width: 19px; height: 19px; }
/* Readable by a screen reader, drawn nowhere. The clip-rect pair is the long-standing
   recipe: display:none and visibility:hidden would take it out of the accessibility
   tree too, which is the opposite of the point. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
}
#layers-btn:hover { background: #f2f2f2; }
#layers-btn.on { background: #2e7d32; border-color: #2e7d32; color: #fff; }

#layers-menu {
  background: rgba(255, 255, 255, .96);
  border: 1px solid #ccc;
  border-radius: 6px;
  padding: 12px 14px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .18);
  min-width: 150px;
}
/* The rows reuse .lands-toggle, which the public-lands checkbox already had; its own
   top margin was for sitting under the Conditions layer buttons and is wrong here. */
#layers-menu .lands-toggle { margin-top: 0; }
#layers-menu .lands-toggle + .lands-toggle { margin-top: 9px; }

/* ---- Phone ------------------------------------------------------------- */
/* Everything above this line is the desktop layout and is untouched by the move to a
   phone-shaped screen; everything below only ever applies under 768px.

   WHAT WAS WRONG. There were no media queries at all, so a 375px screen got the desktop
   layout verbatim: #topbar is a flex row with `flex-wrap`, so it folded into four rows
   about 210px tall -- a quarter of the screen -- and #cond-panel, pinned at `top: 64px`
   for a ONE-row bar, ended up underneath it with its heading and first button hidden.
   The panel then ran most of the way down the screen with no way to dismiss it, so the
   map, which is the whole app, was barely visible.

   THE SHAPE NOW. Chrome at the two edges a thumb can reach, map in between: date and
   search on one row at the top, species chips under them, the three modes as a tab bar
   along the bottom. The map is full-bleed behind all of it. */
@media (max-width: 768px) {
  /* One number, used by everything that has to sit clear of the tab bar. env() adds the
     home-indicator strip on an iPhone, which is 0 everywhere else. */
  :root {
    --tabbar-h: calc(56px + env(safe-area-inset-bottom, 0px));
    --topbar-h: 104px;            /* two rows plus padding; see #topbar below */
  }

  /* THE PAGE MUST NOT SCROLL. The map fills the screen and every piece of chrome sits
     on top of it, so there is nothing to scroll TO -- and when the document did grow
     taller than the viewport, the absolutely-positioned bar and sheet went with it:
     the top bar scrolled away off the top, the "bottom" sheet stranded itself in the
     middle of the page with map showing underneath, and only the tab bar, which was
     already `fixed`, stayed where it belonged. Pinning all three to the VIEWPORT makes
     that picture impossible however the document ends up sized -- which matters on a
     phone, where the browser's own toolbars resize the viewport as you scroll.
     Research scrolls inside its own panel, not the page. */
  html, body { overflow: hidden; }

  #topbar {
    position: fixed;
    top: 0; left: 0; right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    padding: 8px 10px;
    border-radius: 0;
    background: rgba(255, 255, 255, 0.97);
    font-size: 13px;          /* the labels; the inputs are set below */
  }
  /* Search takes the room left over; the date is only as wide as a date. */
  #bar-row { gap: 8px; }
  #search-wrap { flex: 1 1 auto; min-width: 0; }
  #search { width: 100%; box-sizing: border-box; }
  #date-field { flex: 0 0 auto; gap: 0; }
  /* ONE CONTROL SHAPE in this bar. The search box and the date picker had the same
     padding and font and still did not match: a date input's intrinsic line box is
     not a text input's, so the two pills came out different heights. An explicit
     height with border-box settles it whatever the platform does inside. */
  #topbar input, #topbar select {
    height: 40px;
    box-sizing: border-box;
    padding: 0 14px;
    font-size: 16px;
  }
  #topbar input[type="date"] { padding-right: 10px; }

  /* THE INPUTS STAY AT 16px, deliberately, while everything around them shrank.
     iOS Safari zooms the whole page in when you focus an input whose text is smaller
     than 16px, and it does not zoom back out afterwards -- you are left on a magnified
     map you have to pinch your way out of, every time you tap the search box. So the
     room came from the padding, the gaps and the LABEL text instead, which cost nothing.
     If the two fields ever still do not fit, shorten the label or drop it before
     touching this number. */

  /* Chips replace the native select, which on a phone opens a picker over the map.
     The id has to be qualified with #topbar: `#topbar label` above sets display on the
     same element, and one id beats one id plus an element, so a bare #species-field
     loses the cascade and the select stays on screen beside the chips. */
  #topbar #species-field { display: none; }
  #species-chips {
    display: flex;
    font-size: 12.5px;
    gap: 7px;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
  }
  #species-chips::-webkit-scrollbar { display: none; }
  #species-chips .chip { padding: 6px 11px; }

  /* In Research the top bar holds nothing -- date, species and search are all map
     controls and are hidden -- so it would draw as an empty white strip. */
  body.research #topbar { display: none; }

  /* The modes, as a bottom tab bar. */
  #tabbar {
    position: fixed;
    top: auto; bottom: 0; left: 0; right: 0;
    height: var(--tabbar-h);
    padding-bottom: env(safe-area-inset-bottom, 0px);
    border-radius: 0;
    background: #fff;
    box-shadow: 0 -1px 6px rgba(0, 0, 0, 0.18);
  }
  #tabbar .mode {
    flex: 1 1 0;
    border: none;
    border-top: 3px solid transparent;
    border-radius: 0;
    background: #fff;
    font-size: 12px;
    color: #607d8b;
    /* Icon ABOVE the label here, which is what a phone tab bar looks like and what
       gives the icon room to be worth drawing. */
    flex-direction: column;
    justify-content: center;
    gap: 3px;
    padding: 4px 2px;
  }
  #tabbar .mode-ico { width: 23px; height: 23px; }
  /* The active tab is marked with colour and a top rule rather than a filled block: a
     filled tab the width of a third of the screen reads as a button you must press. */
  #tabbar .mode.on {
    background: #fff;
    border-top-color: #2e7d32;
    color: #2e7d32;
    font-weight: 600;
  }

  /* BOTH left-hand panels become a bottom sheet above the tab bar instead of a floating
     card over the map. Tap the heading to collapse it out of the way -- toggled in
     app.js, which binds every `.panel h2` for the same reason this rule is on `.panel`:
     the Regulations key and the Conditions panel are one component now. */
  .panel {
    position: fixed;          /* the viewport's bottom, not the document's */
    top: auto; left: 0; right: 0;
    bottom: var(--tabbar-h);
    max-width: none;
    max-height: 46vh;
    overflow-y: auto;
    border: none;
    border-top: 1px solid #ccc;
    border-radius: 12px 12px 0 0;
    box-shadow: 0 -2px 10px rgba(0, 0, 0, .18);
  }
  .panel h2 {
    cursor: pointer;
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
  /* The chevron is CSS rather than markup so the desktop panel, which does not collapse,
     does not carry a control that does nothing. */
  .panel h2::after {
    content: "▾";
    color: #90a4ae;
    font-size: 13px;
  }
  /* Collapsed means "heading only". Hiding the siblings rather than capping the height
     lets the card shrink to whatever its heading needs, so nothing is half-clipped --
     a 44px cap left the LEGEND sub-heading peeking out below the title.

     `!important` because this has to beat `#legend span { display: flex }`: an id plus
     an element outranks any number of classes, so without it the key's four swatch rows
     stayed on screen and "collapsed" took the panel from 182px to 151px instead of to
     its heading. The mode rules above carry !important for the same reason. */
  .panel.collapsed > *:not(h2) { display: none !important; }
  .panel.collapsed h2::after { content: "▴"; }

  /* Research fills the screen between the two bars. */
  #research-panel {
    position: fixed;
    top: 0;
    bottom: var(--tabbar-h);
    padding: 16px 14px 28px;
  }
  /* The month-by-month grid is the POINT of this tab and was simply off the right edge,
     with river names visible and no data. It scrolls sideways now. */
  #res-body { overflow-x: auto; -webkit-overflow-scrolling: touch; }


  /* MapLibre's own controls. The zoom buttons sat under the top bar and the attribution
     under the tab bar; both are given the room the chrome takes. */
  .maplibregl-ctrl-top-right { top: calc(var(--topbar-h) + 8px); }
  /* Below the top bar AND the zoom buttons under it: 8px gap + their 97px stack. */
  #layers-ctl { position: fixed; top: calc(var(--topbar-h) + 113px); }
  body.research #layers-ctl { display: none; }
  /* The credit line and the scale bar clear the COLLAPSED sheet, which is what now
     occupies the bottom of the map. 78px is that sheet measured: the 44px cap plus its
     14px of padding top and bottom, rounded up. Expanded, the sheet does cover them --
     acceptable, because expanding is a deliberate act the reader can undo, and the
     credit is back the moment they collapse it. */
  .maplibregl-ctrl-bottom-right,
  .maplibregl-ctrl-bottom-left { bottom: calc(var(--tabbar-h) + 78px); }
  /* ---- the map detail SHEET (phone only) -------------------------------
     Built by createMapSheet() in app.js and only ever created below 768px, so these
     rules live inside the phone query with the rest of the sheet behaviour.

     Above the tab bar, not over it: the bar is how you get between Conditions,
     Regulations and Research, and a detail panel should not take the app's navigation
     away. That is also what makes "full" 92% rather than 100% -- the sliver of map left
     above says this is still a map. */
  .map-sheet,
  .map-sheet-closing {
    position: fixed;
    left: 0; right: 0;
    bottom: var(--tabbar-h);
    /* 10, matching #topbar and #layers-ctl, and appended to <body> last so it paints
       over both. The layers button is z-index 10 and was showing THROUGH the sheet at
       full height. Covering it is right: geometry already keeps the sheet below the
       top bar, so the only thing this stacking hides is a control the sheet is sitting
       on top of. Search results stay at 11 and remain above the sheet, which is what
       you want if you search while one is open. */
    z-index: 10;
    display: flex;
    flex-direction: column;
    background: #fff;
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -3px 18px rgba(0, 0, 0, .22);
    overflow: hidden;
    touch-action: none;           /* the drag is ours; the scroll box opts back in */
  }
  /* The 220ms it spends sliding shut. Same box, but it is on its way out: it must
     not answer a tap meant for the sheet replacing it. */
  .map-sheet-closing { pointer-events: none; }
  /* The grab area is 28px tall because it is a drag target for a thumb, not a
     decoration -- the 4px bar inside it is just what you see. */
  .sheet-grab {
    flex: 0 0 auto;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: grab;
  }
  .sheet-grab span {
    width: 40px; height: 4px;
    border-radius: 2px;
    background: #c7d0cc;
  }
  .sheet-close {
    position: absolute;
    top: 4px; right: 6px;
    z-index: 2;
    width: 30px; height: 30px;
    border: 0; background: none;
    font-size: 21px; line-height: 1;
    color: #5b6b65;
    cursor: pointer;
  }
  /* The content. `maplibregl-popup-content` is on this element too, so every rule
     written for popup content applies unchanged; these few lines undo only the parts
     that were about being a floating box. */
  /* The PINNED head: title, then the tab strip. Outside the scroll box, so it stays
     put while a long regulation scrolls under it. A hairline rule rather than a
     shadow -- a shadow here reads as a second sheet edge. */
  .sheet-head {
    flex: 0 0 auto;
    padding: 0 16px;
    border-bottom: 1px solid #e3e8e5;
    /* STATE THE FONT. The page sets none on <body>, so everything here inherits the
       browser default -- which is a SERIF. It never showed before because the title
       lived inside .maplibregl-popup-content, which sets `font: 13px/1.5 system-ui`;
       lifting it out to pin it dropped it straight back to Times. */
    font-family: system-ui, sans-serif;
  }
  .sheet-title {
    margin: 0 34px 8px 0;          /* clear of the close button */
    font-size: 17px;
    line-height: 1.25;
    color: #1565c0;
  }
  .sheet-title[hidden] { display: none; }
  /* The tabs scroll sideways rather than wrap: a second row would push the content
     down on every sheet to serve the rare long label. */
  .sheet-tabs {
    display: flex;
    gap: 18px;
    overflow-x: auto;
    scrollbar-width: none;
    touch-action: pan-x;
  }
  .sheet-tabs::-webkit-scrollbar { display: none; }
  .sheet-tabs[hidden] { display: none; }
  .sheet-tabs button {
    flex: 0 0 auto;
    padding: 2px 0 9px;
    border: 0;
    border-bottom: 3px solid transparent;
    background: none;
    font: 600 14px/1.2 system-ui, sans-serif;
    color: #6b7a74;
    cursor: pointer;
  }
  .sheet-tabs button.on {
    color: #1b5e20;
    border-bottom-color: #2e7d32;
  }
  /* The head owns the top padding now, so the scroll box starts flush under it. */
  .map-sheet .sheet-scroll,
  .map-sheet-closing .sheet-scroll { padding-top: 12px; }

  /* TWO classes, because one is not enough: `.maplibregl-popup-content { max-width:
     78vw }` lives further down this same media query, and at equal specificity the
     later rule won. The sheet's content was being capped at 78vw -- a 83px bar of
     white down the right-hand side on a 375px phone, with the scrollbar floating
     inside it instead of at the screen edge. */
  .map-sheet .sheet-scroll,
  .map-sheet-closing .sheet-scroll {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;
    touch-action: pan-y;          /* scrolling works; sideways gestures stay the map's */
    padding: 0 16px 18px;
    max-width: none;              /* overrides the 78vw cap popups get on a phone */
  }
  /* A popup's title sits under a close button in its own corner; here the button is in
     the sheet's corner, so the title needs the room instead. */
  .sheet-scroll h3 { font-size: 17px; margin: 0 34px 8px 0; }
  .sheet-scroll .desc { font-size: 13.5px; }

  /* Research has no map, so nothing needs to dodge a sheet there. */
  body.research .maplibregl-ctrl-bottom-right,
  body.research .maplibregl-ctrl-bottom-left { bottom: calc(var(--tabbar-h) + 2px); }
  body.research .maplibregl-ctrl-top-right { top: 8px; }

  /* A popup sized for a desktop pointer overflows a 375px screen. */
  .maplibregl-popup-content { max-width: 78vw; }
}

/* ---- Laptop and desktop: spend the width --------------------------------------
   Everything above was written for a narrow card and is shared by every screen. The
   phone block has its own overrides; the LAPTOP had none, so a 1440px window drew the
   same 214px ribbon as a 420px one and left 1,200px of map furniture empty.

   Nothing in here applies below 1024px, so the phone layout cannot be changed by it.

   Scoped to #cond-panel, NOT .panel. The Regulations legend is the other .panel, and
   its rows are "Open", "Closed now" -- it is already the right size and widening it
   would only make an emptier box. The 180px on .panel is the score ramp measured
   exactly (see the note there); the ramp keeps that width inside a wider card, because
   it sizes to its content, so the end labels stay under the ends of the bars. */
@media (min-width: 1024px) {
  #cond-panel { max-width: 320px; }

  /* The layer menu stays a single COLUMN here, same as everywhere else. It was briefly
     a two-column grid to save height; the user preferred them stacked, and with the
     Gauges entry now behind ?dev=1 there are five rather than six, so the height it was
     buying back is largely gone anyway. The panel still reads better wide, because the
     legend prose underneath stops wrapping every three words -- that was always the
     bigger half of the win. */

  /* SPECIES AS CHIPS, not the dropdown. The phone has shown chips all along and the
     desktop a <select>; there is room for the chips in a full-width bar, and one
     control in both places is one less thing to keep in step.

     The <select> is NOT removed, only hidden -- it is still the model. Four places read
     `#species`.value and the change listener is bound to it; the chips write to it and
     fire its "change", so everything downstream runs exactly as before.

     `#topbar #species-field` carries the id twice for the same reason the phone block
     does: `#topbar label` above sets display on this very element, and one id plus an
     element beats one id on its own, so a bare #species-field loses the cascade and the
     dropdown stays on screen next to the chips. */
  #topbar #species-field { display: none; }
  #species-chips { display: flex; gap: 8px; }

  /* ONE BAR ACROSS THE TOP, instead of two cards at opposite ends with 400px of map
     between them. The tabs keep the left corner and the controls keep the right; what
     changes is that the white card now runs the whole way, so the two read as ends of
     one bar rather than as unrelated furniture.

     Done WITHOUT touching the HTML. #topbar and #tabbar are 260 lines apart in the
     document -- the nav sits near the foot, after the panels -- so making them real
     siblings in one flex row would mean moving the nav, which changes keyboard order:
     on a phone the tabs are at the BOTTOM of the screen and are read last, which is
     correct, and moving them would make them read first. Stretching the bar and letting
     the tabs sit on top of its left end costs nothing and breaks nothing: the tabs are
     z-index 11 and the bar is 10, so they already draw above it. */
  #topbar {
    left: 10px;                /* was auto, so the card only grew leftward from `right` */
    right: 60px;               /* unchanged -- still clear of MapLibre's zoom control */
    justify-content: flex-end; /* the controls stay at the right end of the long bar */
  }
  /* Indented from the bar's own corner so the tabs look set INTO it, and the drop
     shadow goes -- it is sitting on a white card now, not on the map.

     17px centres a 34px tab bar in the 48px bar: 10 + (48 - 34) / 2. Both numbers are
     measured, not guessed -- the bar is 8px of padding either side of a 32px input, and
     it is the same 48 in every mode because Conditions only hides the search, never a
     taller control. If either height changes, this moves with it. */
  #tabbar {
    top: 17px;
    left: 24px;
    box-shadow: none;
  }
  /* Research hides every control in the bar -- they are all `.map-only` -- so a
     full-width bar would draw as a blank white strip across the top. It is an empty
     pill today too, just a small one nobody notices. The phone block already hides it
     here for exactly this reason. */
  body.research #topbar { display: none; }

  /* NEVER TALLER THAN THE WINDOW. The panel's height is not fixed -- it is whatever the
     selected layer's legend needs -- and the range is wide: 438px for Current
     Conditions against 647px for Gauges, measured. At 647 it runs off the bottom of a
     1366x700 laptop by 11px, and what it loses is the foot of the legend, which is the
     part that explains the colours.

     So cap it at the window and let it scroll inside itself. The cap only engages when
     the content would overflow, so on a tall screen nothing changes and no scrollbar
     appears.

     64px is .panel's own `top`; the 12px matches its `left`, so the gap under the panel
     equals the gap beside it rather than sitting flush against the bottom edge.

     On .panel, so the Regulations legend is covered by the same rule. It is 171px today
     and nowhere near the cap -- this is the rule that stops it mattering if it grows. */
  .panel {
    /* BORDER-BOX IS LOAD-BEARING, not tidiness. .panel has 14px of padding top and
       bottom and a 1px border, and max-height applies to the CONTENT box by default --
       so a 624px cap drew a 654px panel, which still ran 18px off the bottom of a
       1366x700 window even though the scrolling had correctly kicked in. Measured, not
       reasoned about: 624 + 28 + 2 = 654 exactly. */
    box-sizing: border-box;
    max-height: calc(100vh - 64px - 12px);
    overflow-y: auto;
  }
}

/* ---- the loading screen ------------------------------------------------------------
   Covers the app while it boots. app.js adds `gone` when the map and the regulation
   colours are both in, and removes the element once the fade has run.

   The container's own background is the point of it: #264f3a is the solid green from
   the bottom of the artwork, so the brand colour is on screen from the first painted
   frame, before splash.svg has finished downloading. The image then fades in over it,
   which is why it starts at opacity 0 -- a half-drawn photo popping in is worse than a
   held colour. If the image never arrives the screen simply stays green, which is a
   reasonable loading screen in its own right.

   z-index 100 is well clear of the app's own stack, which tops out at 11. */
#splash {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: #264f3a;
  opacity: 1;
  transition: opacity .45s ease;
}
#splash img {
  width: 100%;
  height: 100%;
  /* `cover`, not `contain`: the artwork is 9:16 and most screens are not, so letterboxing
     it would put bars of a DIFFERENT green beside the photo. Cover crops instead, and
     because the logo sits near the middle of the canvas it survives the crop on both a
     tall phone and a wide desktop. */
  object-fit: cover;
  object-position: center;
  display: block;
  opacity: 0;
  transition: opacity .4s ease;
}
#splash img.ready { opacity: 1; }

/* Fading OUT. `pointer-events: none` matters as much as the opacity: without it the
   splash would still be swallowing taps for the length of the fade, on a screen that
   already looks like the map. */
#splash.gone {
  opacity: 0;
  pointer-events: none;
}

/* Someone who has asked their system for less motion gets the same screen without the
   cross-fades -- it appears and it goes. */
@media (prefers-reduced-motion: reduce) {
  #splash, #splash img { transition: none; }
}
