@import url('tokens.css?v=9de0541');

/* Control shell: the header and left panel sit on --dark, they are
   control elements, not content. Content (map, table, location card)
   stays light - it's the only glowing surface, the dark shell just holds
   the frame around it. No gradients, no shadow into a glow - just a flat
   color. */
.hdr, #panel { background: var(--dark); color: var(--white); }

.hdr { display: flex; align-items: stretch; border-bottom: 1px solid var(--dark-line); }
.hdr__brand { padding: var(--space-3) var(--space-4); font-weight: 700; letter-spacing: .02em;
  display: flex; align-items: center; color: var(--white); text-decoration: none; }
.hdr__brand:hover { color: var(--accent-dark); }
.hdr__search { flex: 1; display: flex; align-items: center; padding: 0 var(--space-4); }
.hdr__search input { width: 100%; max-width: 420px; border: 1px solid transparent;
  border-radius: var(--radius); padding: var(--space-2) var(--space-3); font: inherit;
  background: var(--white); color: var(--text); }
.hdr__search input::placeholder { color: var(--text-muted); }
.hdr__nav { display: flex; }
.hdr__nav a, .hdr__lang a { display: flex; align-items: center; padding: 0 var(--space-4);
  text-transform: uppercase; font-size: 13px; font-weight: 600; letter-spacing: .08em;
  text-decoration: none; color: var(--text3); border-left: 1px solid var(--dark-line); }
.hdr__nav a[aria-current='page'] { color: var(--accent-dark); box-shadow: inset 0 -3px 0 var(--accent-dark); }
/* The language link sits after the navigation and looks like it. On a
   phone it moves into the search row: the first row is full, and one
   more link there would push the header onto a third line. */
.hdr__lang { display: flex; }

/* The height of .layout MUST NOT be derived from a hardcoded header
   height - on a narrow viewport the header wraps to two lines (96 px
   instead of 60 px, see the .hdr media query below), and a fixed
   "100vh - 60px" then stretches the document by the difference, so the
   page scrolls vertically and the map wobbles along with the document
   when dragged with a finger. Instead of computing with a specific
   number, the header/panel height is kept by flexbox ordering alone:
   body is a column flex container exactly the height of the window
   (html/body height 100%, see below), the header is flex:none (it takes
   exactly what it needs - 1 or 2 lines), .layout gets the rest
   (flex: 1 1 auto). The result is the same on a wide viewport (where the
   header doesn't wrap) and a narrow one, with no dependency on a
   specific px value. `body` must have a FIXED `height: 100%`, not just
   `min-height` - with plain min-height, flexbox has no definite height
   for .layout (flex-shrink + min-height:0) to shrink into: on
   seznam.html with 1372 rows the table would push #main and .layout to
   their full content height (roughly 216 000 px) and the whole document
   would scroll as one instead of an internal scroll in #main. */
html { height: 100%; }
body { height: 100%; display: flex; flex-direction: column; }
.hdr { flex: none; }
/* position: relative because of the detail panel, which is its
   absolutely positioned descendant. It can't hang off #main: on the list
   page #main is a scrolling container (overflow:auto), so the panel
   would anchor to the END of the 1372 rows instead of the right edge of
   the screen. The right edge of .layout is the same as the right edge of
   #main (the last grid column), so nothing changes on the map. */
.layout { display: grid; grid-template-columns: 300px 1fr; flex: 1 1 auto; min-height: 0;
  position: relative; }
#panel { overflow-y: auto; }
#main { position: relative; }

/* The map base (OSM tiles) is a full-color foreign raster that competes
   for attention with the pins. Desaturating the entire
   .leaflet-tile-pane would work, but on a flood-level portal the
   river's course is the context in which the stations even make sense,
   and desaturating it along with everything else would make it
   disappear too. Solution: two tile layers stacked on top of each
   other (map.js), each with its own filter on the LAYER'S CLASS
   (className), not on .leaflet-tile-pane - the parent pane's filter
   would be applied AFTER the child's filter and would also desaturate
   the water layer, because .tiles-water would be its descendant. The bottom
   one (.tiles-base) is almost fully black and white. The top one (.tiles-water)
   uses an SVG feColorMatrix filter #water-only (defined in an <svg>
   directly in the document - it must be in the HTML, not in this
   external css file, so the browser can find it), which converts a
   pixel's blueness into its TRANSPARENCY (not into a color) - it only
   lets through pixels where blue clearly exceeds both red and green.
   The rest stays transparent, so the desaturated .tiles-base underneath
   shows through it. Result: a black-and-white base with highlighted
   water, with no change to color rendition (the water is still "OSM
   blue", just more saturated - saturate() after the filter). The filter
   is a matrix of numbers (feColorMatrix), not a named/hex color - it's
   not affected by the named-color test (smoke.test.js).
   Side effect: the filter also catches other strongly blue OSM base
   elements, e.g. the dashed national border (OSM draws it purple-blue) -
   acceptable, not worth fixing at the cost of complexity.
   The pins live in a different pane (marker), so none of these filters
   affect them - they stay full color. */
.leaflet-tile-pane .tiles-base { filter: grayscale(100%) brightness(1.06) contrast(.95); }
.leaflet-tile-pane .tiles-water { filter: url(#water-only) saturate(2.4); }

/* Focus ring: on a dark background the browser's default ring blends
   into it, hence a custom one - light blue on --dark, dark blue
   elsewhere. outline-offset keeps the header/panel ring over the dark
   background, not over a white filled field (input, checkbox) - there it
   would only have about 2:1. */
:focus-visible { outline: 2px solid var(--blue-dk); outline-offset: 2px; }
.hdr :focus-visible, #panel :focus-visible { outline-color: var(--accent-dark); }

/* Eyebrow from the reference: a short line + letter-spaced uppercase
   label. Used only where it carries information the reader wouldn't
   otherwise have (station type, section name in the panel) - not as a
   frame around every paragraph. */
.eyebrow { display: flex; align-items: center; gap: var(--space-2);
  text-transform: uppercase; font-size: 11px; font-weight: 600; letter-spacing: .12em;
  color: var(--blue); margin: 0 0 var(--space-2); }
.eyebrow::before { content: ''; width: 18px; height: 2px; background: var(--blue); }
/* In the panel the eyebrow sits on --dark - --blue would nearly
   disappear there (2.9:1). */
#panel .eyebrow { color: var(--accent-dark); }
#panel .eyebrow::before { background: var(--accent-dark); }

.btn { display: inline-flex; align-items: center; gap: var(--space-2);
  background: var(--blue); color: var(--white); border: 0; border-radius: var(--radius);
  padding: var(--space-2) var(--space-4); text-transform: uppercase; font-size: 13px;
  font-weight: 600; letter-spacing: .08em; cursor: pointer; text-decoration: none; }
.btn:hover { background: var(--blue-dk); }

/* Cards separated by a hairline, not a shadow. .hairline is only used
   in the panel (on --dark) - gray-line would look like a glowing line
   there. */
.hairline + .hairline { border-top: 1px solid var(--dark-line); }
.card { padding: var(--space-3) var(--space-4); }

/* Station count and stale-data notice: MUST be visible whether the
   collapsible filter panel (<details> below) is open or closed - on a
   narrow viewport, i.e. the main device during a flood, it starts
   collapsed (panel-toggle.js), so anything inside <details> would stay
   hidden. Placed as a bar BEFORE <details>, outside the collapsible
   part - works the same on wide and narrow viewports, no extra media
   query needed (unlike a solution via <summary> content, which is
   hidden on a wide viewport - see #panel summary below - and would need
   separate handling for both widths). */
.panel-status { padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--dark-line); }
.panel-status #count { margin: 0; }
.panel-status .stale-notice { margin: var(--space-1) 0 0; }

/* Collapsible filter panel: native <details>/<summary> - the browser
   handles the click, keyboard, and aria-expanded itself, no custom JS
   state needed. The summary is visible only on a narrow viewport (media
   query below) - on a wide one the panel always stays expanded
   (js/panel-toggle.js forces `open`), so the hidden summary doesn't
   matter there. */
#panel summary { flex: none; cursor: pointer; list-style: none; display: flex; align-items: center;
  justify-content: space-between; gap: var(--space-2); padding: var(--space-3) var(--space-4);
  text-transform: uppercase; font-size: 13px; font-weight: 600; letter-spacing: .08em; }
#panel summary::-webkit-details-marker { display: none; }
#panel summary::marker { content: ''; }
/* The arrow carries the expand state (direction), not the color - the
   same principle as the trend arrows on station values. The accent-dark
   color is designated precisely for markers on a --dark background (see
   tokens.css).

   Drawn in CSS, not as the '▾' character (U+25BE): a "small triangle"
   glyph only fills part of its em-box, so at a given font-size it
   renders smaller than that size promises and is barely visible as a
   control on a phone; the rendered size also depends on which font
   supplies the character. A triangle made from a border has an exact
   size, independent of the font.

   The clickable area is the whole <summary> (~52 px tall), the arrow is
   just its visual marker - so it's enlarged for visibility, not for
   hit-target size. The triangle is 16 px wide and 9 px tall, and with
   the side offsets it takes up 24 px. */
#panel summary::after {
  content: '';
  flex: none;
  width: 0;
  height: 0;
  margin: 0 4px;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-top: 9px solid var(--accent-dark);
  transition: transform .15s;
}
#panel details[open] > summary::after { transform: rotate(180deg); }

/* On a wide viewport there's nothing to collapse - the panel is a side
   column next to the map/table, not an overlay sheet. Summary hidden,
   js/panel-toggle.js keeps <details> always open (see there); if the
   script fails to run for some reason, the HTML already has `open` at
   load time, so the content stays visible. */
@media (min-width: 769px) {
  #panel summary { display: none; }
}

/* Label alignment: flexbox with a gap and its own row. */
#panel label { display: flex; align-items: center; gap: var(--space-2); margin-bottom: var(--space-2); }
#panel #count { color: var(--text3); }

/* Legend in the panel: each item on its own row, inline elsewhere. */
#panel .lvl-badge { display: flex; margin-bottom: var(--space-2); }
/* The dot's outline (below) is --marker-border - clearly visible on a
   light background (map, table), but in the panel the background is
   already --dark and close to it in lightness, so the outline would
   nearly disappear and grades 0 and 3 would blend into the background
   (measured 3.7:1 instead of a visibly distinct border). Solution
   without changing the palette: a white ring behind the marker
   ("a different background under the marker"), not a change to
   --dot-border-color - that stays shared across the map, table, and
   panel.
   With the CHMU palette (see tokens.css) the fill alone against --dark
   has 3,32:1 (grade 0) and only 2,93:1 (grade 3, dark red) - both under
   a safe margin, so the ring is needed for the current palette too, not
   just a leftover from an earlier one. */
#panel .lvl-badge::before { box-shadow: 0 0 0 2px var(--white); }

.lvl-badge { display: inline-flex; align-items: center; gap: var(--space-2); font-size: 13px; }
/* Color isn't the only carrier of grade: the outline's thickness grows
   monotonically with severity - the same scale as the pin on the map,
   so the legend (and the status column in the table) matches what's
   visible on the map even in black and white. The outline grows OUTWARD
   (box-sizing: content-box overrides the global border-box reset just
   for this dot), not inward - otherwise at III. SPA the outline's
   thickness would eat almost the entire colored fill and the most
   severe grade would look the quietest. The colored fill
   (--dot-fill) thus grows along with the outline and stays dominant at
   all four levels - its diameter is at least 55 % of the marker's total
   diameter. */
.lvl-badge::before {
  content: '';
  box-sizing: content-box;
  flex: none;
  width: var(--dot-fill);
  height: var(--dot-fill);
  border-radius: 50%;
  background: var(--dot);
  border: var(--dot-border) var(--dot-style, solid) var(--dot-border-color);
}
/* --dot-fill = diameter of the colored fill, --dot-border = thickness of
   the outward-growing outline. The marker's total diameter = fill +
   2*border (10, 13, 16, 19 px), the fill/total ratio decreases 80 % ->
   69 % -> 63 % -> 58 %, always above the required 55 %. */
.lvl-0    { --dot: var(--lvl-0); --dot-fill: 8px;  --dot-border: 1px;   --dot-border-color: var(--marker-border); }
.lvl-1    { --dot: var(--lvl-1); --dot-fill: 9px;  --dot-border: 2px;   --dot-border-color: var(--marker-border); }
.lvl-2    { --dot: var(--lvl-2); --dot-fill: 10px; --dot-border: 3px;   --dot-border-color: var(--marker-border); }
.lvl-3    { --dot: var(--lvl-3); --dot-fill: 11px; --dot-border: 4px;   --dot-border-color: var(--marker-border); }
/* Marker shape = station type, shown directly next to the type filter.
   Square only for rain gauges, circle for everything else - must match
   icon() in map.js (`s.type === 'SR' ? border-radius:2px : 50%`),
   otherwise the filter lies; checked by test/smoke.test.js.

   The fill is white and the outline is the same as the marker on the
   map. No color from the state scale may appear here - shape carries
   type, color carries grade, and a colored sample would mix those two
   carriers. White also doesn't collide with "no data", which is hollow
   with a dashed outline.

   aria-hidden: for a screen reader this is decoration. The filter's
   name ("Hladina") already says what it's about, and "circle" would add
   nothing to it - it would just be read out on every pass through the
   filter.

   margin-left: auto pushes the shapes to the right edge, so they stack
   in a single column and read as a legend next to the filter, not as
   part of the label. */
/* Label above the shape column, by the customer's decision (9 Sept
   2026). Without it, the shapes next to the checkboxes read as another
   control, not as a legend - two columns under one heading have no way
   to say that each means something different. */
/* The label is pushed to the right via margin-left:auto, not via
   justify-content: space-between - that would space all three elements
   out evenly and the heading would detach from its dash (::before)
   towards the center, while every other eyebrow keeps it right after
   it. */
.eyebrow--split { display: flex; align-items: center; gap: var(--space-2); }
.eyebrow__note { margin-left: auto; text-transform: uppercase; font-size: 11px;
  letter-spacing: .08em; font-weight: 600; color: var(--text3); }
#panel .eyebrow--split::before { flex: none; }

.shape { flex: none; margin-left: auto; width: 10px; height: 10px;
  box-sizing: content-box; background: var(--white);
  border: 2px solid var(--marker-border); box-shadow: 0 0 0 2px var(--white); }
.shape--round { border-radius: 50%; }
.shape--square { border-radius: 2px; }

/* No data: hollow, dashed, total diameter (10 px) the same as grade 0 -
   it doesn't look more severe than the normal state. */
.lvl-none { --dot: transparent; --dot-fill: 8px; --dot-border: 1px; --dot-border-color: var(--lvl-none); --dot-style: dashed; }

@media (max-width: 768px) {
  .layout { grid-template-columns: 1fr; }
  /* #panel has no max-height of its own - the collapsed panel (just the
     summary) is exactly as tall as the summary itself, and the cap at
     55 % of screen height sits directly on .panel-body (which scrolls
     on its own), not on #panel as a whole. flex-shrink on #panel/<details>
     does not work reliably here - max-height without an explicit height
     does not force flex-shrink descendants to actually shrink, it just
     clips the outer box (the content then silently overflows under
     overflow:hidden, unreachable by scrolling). A direct max-height on
     .panel-body avoids that. */
  /* On a narrow viewport the panel is a sheet at the bottom edge, but it
     stays IN FLOW: `position: fixed` would take it out of flow, so the
     scrolling #main would not know about it and the last list card
     would scroll underneath it, its link unreachable. Reserving space
     by computing the panel's height from JS is fragile: the first
     measurement happens while still in the system font, and once
     Poppins finishes loading and the panel grows, ResizeObserver does
     not reliably report that change. A column flex handles it without a
     single measurement - the browser does the height split itself and
     it holds for every font and even when the result-count text wraps.
     env(safe-area-inset-bottom) keeps the content above the gesture bar
     on phones without a physical button. */
  .layout { display: flex; flex-direction: column; }
  #main { order: 1; flex: 1 1 auto; min-height: 0; }
  #panel { order: 2; flex: none; overflow: visible;
    border-top: 1px solid var(--dark-line); box-shadow: 0 -2px 16px rgb(0 0 0 / .35);
    padding-bottom: env(safe-area-inset-bottom, 0px); }
  #panel .panel-body { max-height: 55vh; overflow-y: auto; }

  /* Narrow viewport: brand and navigation on the first line (narrowed,
     but legible), search on its own line below them so it doesn't need
     a minimum width. The ::after is an empty full-width item between the
     navigation and the search: it breaks the line there at every width,
     so the search needs no width of its own to be pushed down - it takes
     what the language links leave, however many there are. */
  .hdr { flex-wrap: wrap; }
  .hdr::after { content: ''; order: 2; flex-basis: 100%; }
  .hdr__brand { order: 1; padding: var(--space-2) var(--space-3); font-size: 14px; }
  .hdr__nav { order: 2; }
  .hdr__nav a { padding: 0 var(--space-2); font-size: 12px; }
  .hdr__search { order: 3; flex: 1 1 10rem; min-width: 0; padding: var(--space-2) var(--space-3); }
  .hdr__lang { order: 4; }
  .hdr__lang a { padding: 0 var(--space-3); font-size: 12px; }
}

/* Data-load failure notice: replaces only the content of the main area
   (the map or the table), the panel with filters and legend next to it
   stays untouched. */
.data-error { padding: var(--space-5) var(--space-4); text-align: center; }
.data-error p { max-width: 480px; margin: 0 auto var(--space-4); color: var(--text); }

/* REFRESH failure (unlike .data-error above) while data is already
   displayed - the map/table stay as they are, this is just an
   unobtrusive reminder that the data is older and up to what time it's
   still valid. Deliberately quiet - --text3, small font, no box or
   color. */
#panel .stale-notice { color: var(--text3); font-size: 12px; margin: var(--space-1) 0 0; }

/* Sorting the list without a table header: the header (click to
   sort) is hidden below 768 px (see .tbl thead below), so on mobile -
   the main device during a flood - there would be no way to sort the
   list. The select is visible at all widths (also works after rotating
   the phone to landscape) and duplicates the same option that clicking
   the header gives on a wide viewport. */
.list-toolbar { padding: var(--space-3); display: flex; align-items: center; gap: var(--space-2); }
.list-toolbar label { display: flex; align-items: center; gap: var(--space-2); font-size: 13px; font-weight: 600; }
.list-toolbar select { font: inherit; padding: var(--space-1) var(--space-2); border: 1px solid var(--gray-line);
  border-radius: var(--radius); background: var(--white); color: var(--text); }

/* Station list table. */
.tbl { width: 100%; border-collapse: collapse; }
.tbl th { text-align: left; text-transform: uppercase; font-size: 11px; letter-spacing: .1em;
  color: var(--text-muted); font-weight: 600; padding: var(--space-2) var(--space-3);
  border-bottom: 1px solid var(--gray-line); cursor: pointer; white-space: nowrap; }
.tbl th[aria-sort] { color: var(--blue); }
.tbl td { padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--gray-line); }
.tbl tbody tr:nth-child(even) { background: var(--gray-bg); }
.tbl .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Column widths of the station list (above 768 px, when the table is
   still a table - below that it's replaced by cards, see the media
   query below). The browser's automatic layout computes widths from
   only some sample of rows (the table has over 1300 rows) and easily
   comes out unbalanced - the Stanice (Station) column gets extra room,
   while Stav (Status) ("II. SPA – pohotovost") or Tok (Watercourse)
   (commonly long names like "Mikulečský potok") wraps to two lines. The
   <colgroup> in seznam.html gives the four columns with bounded content
   (number, status badge, date format) a fixed width measured from their
   longest real value + padding; Stanice stays without a width (auto)
   and gets all the rest - it's the only column whose text length isn't
   bounded from above. min-width on the table keeps Stanice above a
   reasonable minimum at narrower desktop widths - #main already has
   overflow:auto, so instead of wrapping, a horizontal scroll kicks in
   inside the panel, not the whole page. Doesn't apply to the
   measurement-history table on the station card (that stays a generic
   .tbl without this class). */
.tbl--stations { table-layout: fixed; min-width: 960px; }
.tbl--stations th, .tbl--stations td { overflow-wrap: break-word; }

/* Cell with the type symbol. In the panel the symbol is right-aligned
   (margin-left:auto on .shape), here it should sit centered in the
   narrow column. */
.cell-shape { text-align: center; }
.cell-shape .shape { display: inline-block; margin-left: 0; vertical-align: middle; }

/* Status dot in the dense table row: the same scale as the legend in
   the panel (8-11px fill, 1-4px outward-growing outline), scaled down
   to 60 % so that III. SPA fits into the row without throwing off its
   height. The scaling is linear, so the fill/total ratio (58-80 %)
   stays the same as in the legend. */
.tbl .lvl-badge::before { width: calc(var(--dot-fill) * .6); height: calc(var(--dot-fill) * .6);
  border-width: calc(var(--dot-border) * .6); }

/* Below 768 px, rows become cards. */
@media (max-width: 768px) {
  .tbl thead { display: none; }
  .tbl, .tbl tbody, .tbl tr, .tbl td { display: block; width: 100%; }
  .tbl tr { border-bottom: 1px solid var(--gray-line); padding: var(--space-2) 0; }
  .tbl td { border: 0; padding: 2px var(--space-3); }
  .tbl td::before { content: attr(data-label) ': '; color: var(--text-muted); font-size: 12px; }
  /* In a card, the symbol on its own row without a label would hang
     there unexplained - it's hidden, because the row "Typ: Hladina" a
     bit further down says the same thing in words. The selector must
     nest .tbl: `.tbl td { display: block }` a few lines above has
     higher specificity than .cell-shape alone and would override it. */
  .tbl .cell-shape { display: none; }
  .tbl .num { text-align: left; }
  /* min-width from .tbl--stations is for the tabular (desktop) layout -
     in cards it would force horizontal scrolling of the whole page. */
  .tbl--stations { min-width: 0; }
}

/* --- Station detail: shared between lokalita.html (full page,
   station.js) and the station panel on the map and the list. Both
   call the same renderStationDetail() function from js/station-view.js,
   which inserts exactly this markup structure into its container - see
   the comments there. */

/* A station name without spaces or hyphens (e.g. from LVS:
   "Most_Vaclava_Rendera_Q2_62100259") has nowhere to wrap - the h1 is
   large and without this exception it pushes the document into
   horizontal scrolling. A break between characters is the last resort,
   normal wrapping at spaces and hyphens still takes priority. */
.station-name { font-size: clamp(28px, 4vw, 44px); font-weight: 700; margin: 0 0 var(--space-2);
  overflow-wrap: break-word; }
/* The gap below the subtitle is kept at space-3 (not space-4) - the
   card has .chvals right below it, which already carries its own
   spacing. */
.station-sub { margin: 0 0 var(--space-3); }

/* Warning about a dead station with a nonzero frozen grade (H Lukov has
   been reporting III. SPA since 2021) - MUST be the first thing on the
   screen, so stationDetailHtml() returns it as the very first item,
   before the station name. The color is --lvl-none (gray), not the
   color of the frozen grade: it's a notice about the value's
   invalidity, not an active alarm - see worstDisplayLevel() in
   severity.js, which ignores dead stations for the same reason. */
.alert-stale { background: var(--gray-bg); border-left: 4px solid var(--lvl-none);
  padding: var(--space-3) var(--space-4); margin: 0 0 var(--space-3); }
.alert-stale__title { font-weight: 700; margin: 0 0 4px; display: flex; align-items: center; gap: var(--space-2); }
.alert-stale p { margin: 0 0 4px; }
.alert-stale p:last-child { margin-bottom: 0; }

/* Channel values: the primary one (station.primary from data.js - for
   12 stations this isn't channels[0], e.g. Pec_Zeleny_potok draws the
   level graph even though the first channel in the data is
   precipitation) comes first and is more prominent, the others come
   after it and are smaller. Only font size/weight distinguishes them,
   not color - color stays reserved for the SPA grade. */
.chvals { display: flex; flex-wrap: wrap; align-items: flex-start; gap: var(--space-3) var(--space-4); margin: 0 0 var(--space-3); }
.chval { margin: 0; }
.chval__label { font-size: 12px; font-weight: 600; color: var(--text-muted); margin: 0 0 2px; }
.chval__row { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--space-3); margin: 0; }
.chval__num { font-weight: 700; font-variant-numeric: tabular-nums; }
.chval__unit { font-size: .45em; font-weight: 600; margin-left: 4px; }
.chval--primary .chval__num { font-size: 40px; }
.chval--secondary .chval__num { font-size: 22px; }
.chval__meta { color: var(--text-muted); font-size: 13px; margin: 2px 0 0; }

/* Trend (data.js: {ratePerHour, direction, windowMin}) carries direction
   via the arrow's SHAPE, not color - color is reserved for the SPA
   grade, and the two must stay independent. So none of the variants
   below change the text color by direction, only the glyph and word.
   direction===null (window > 6 h or too few points) makes trendHtml()
   return an empty string - nothing is shown at all, not a zero or an
   arrow. 'steady' is a valid state and
   must look different from a missing trend, so it has its own label
   with no number. */
.trend { display: inline-flex; align-items: center; gap: 4px; font-size: 15px; font-weight: 600;
  color: var(--text); white-space: nowrap; }
/* 'steady' is distinguished from rising/falling only by the glyph and
   word (see the comment above), NOT by lower contrast - --text3 has
   only 2,9:1 against a white background, below the required 4,5:1, and
   the trend is too important a piece of information to be less legible
   than the rest of the card. */
.trend__arrow { font-size: 1.2em; line-height: 1; }

.graph { max-width: 100%; height: auto; border: 1px solid var(--gray-line); }

/* The graph is clickable - the button around it must not look like a
   button, but it must be recognizable that it can be clicked. Hence the
   cursor, a subtle border highlight on hover, and a normal focus ring
   for the keyboard. Display block and zero padding, so the wrapper
   doesn't change the layout compared to when there was no button
   there. */
.graph-zoom { display: block; width: 100%; padding: 0; border: 0; background: none;
  cursor: zoom-in; border-radius: var(--radius); }
.graph-zoom:hover .graph { border-color: var(--blue); }

/* Enlarged graph. <dialog> lets the browser handle Esc, the focus trap,
   and the inert background; only size and background are left here.
   max-width/height in percent of the viewport, so the graph fits even a
   phone in landscape - the image is 700 px wide, narrower displays must
   be able to shrink it. */
/* No display here: any value would override the browser's own
   dialog:not([open]) { display: none } and leave the closed dialog
   drawn at the top of the page. */
.graph-dialog { max-width: 96vw; max-height: 92vh; padding: var(--space-3);
  border: 1px solid var(--gray-line); border-radius: var(--radius);
  background: var(--white); color: var(--text); }
.graph-dialog::backdrop { background: rgb(0 0 0 / .55); }
/* The graph is shown enlarged at its ORIGINAL size (700 px), not shrunk
   to the window's width. On a phone that's the whole point of opening
   it: in the panel the graph has under 390 px and you can read the
   axis labels only with luck; shrinking it again to the display's width
   would gain nothing. A narrower viewport therefore scrolls it
   horizontally - only the image scrolls, the bar with the close button
   stays put. */
.graph-dialog__scroll { overflow: auto; max-height: 78vh; }
.graph-dialog__img { display: block; max-width: none; height: auto;
  border: 1px solid var(--gray-line); }
.graph-dialog__bar { display: flex; justify-content: flex-end; margin: 0 0 var(--space-2); }
.graph-dialog__close { font-size: 24px; line-height: 1; width: 44px; height: 44px;
  border: 0; background: none; color: var(--text); cursor: pointer; border-radius: var(--radius); }
.graph-dialog__close:hover { background: var(--gray-bg); }
.graph-note { color: var(--text-muted); font-size: 13px; margin: var(--space-2) 0 0; }
.minimap { height: 280px; border: 1px solid var(--gray-line); }
.no-location { color: var(--text-muted); }

/* Section separation (graph, measurement table, map) is held by a
   hairline and spacing, not a repeated eyebrow label - "PRUBEH" (course)
   and "POLOHA" (location) would carry no extra information, they would
   just repeat what the content already says on its own. Spacing
   space-3 (not space-4) - with two separate lines that is enough for
   the vertical rhythm. */
.section-rule { border: 0; border-top: 1px solid var(--gray-line); margin: var(--space-3) 0; }
.subhead { font-size: 13px; font-weight: 600; color: var(--text-muted); margin: 0 0 var(--space-2); }

/* --- Station detail panel over the map (index.html, map.js) ---
   Clicking a pin keeps the map's context - the detail opens next to it
   (wide canvas) or from the bottom (narrow), not as a return to a
   separate page. The standalone URL (lokalita.html) is still kept for
   sharing links. The panel is absolutely positioned inside #main
   (position: relative), whose size doesn't change because of it - the
   map underneath stays fully rendered and usable, the panel just
   overlaps the edge.

   z-index 1100: vendor/leaflet/leaflet.css gives the map's controls
   (zoom, attribution) z-index 1000 (.leaflet-top/.leaflet-bottom). A
   value below that would let the zoom button overlap the panel's
   corner on a narrow viewport, and the "Leaflet | (c) OpenStreetMap"
   attribution sit over the measurement table in the panel on desktop.
   When the panel is open, it's precisely what the user needs - so it
   must stand ABOVE the map's ambient controls, not below them. */
/* The panel's width is held by --panel-w, which js/panel-resize.js sets
   while dragging the left edge. The default 420 px applies until the
   user drags it (and always on a narrow viewport - there the panel is a
   sheet at the bottom edge, the width can't be changed and the handle
   is hidden). */
.station-panel { position: absolute; top: 0; right: 0; bottom: 0;
  width: min(var(--panel-w, 420px), 100%);
  background: var(--white); box-shadow: -2px 0 16px rgb(0 0 0 / .18); z-index: 1100;
  border-left: 1px solid var(--gray-line); display: flex; flex-direction: column; }
/* The width-resize handle sits on the panel's left edge. The hit area is
   14 px wide - hitting a thin line with a mouse is annoying and
   impossible with a finger - but only a narrow strip in the middle of
   the height is VISIBLE.

   That strip is there always, not only on hover, by the customer's
   decision (9 Sept 2026): showing it only under the cursor leaves
   discovery to accident, and for a control with no name other than its
   own shape, that's a reliable way to hide it.

   On hover and focus it turns blue and grows, so it's recognizable as
   active. The focus ring must be visible on both sides - the map or
   table (light) is on the left, the panel (white) on the right - hence
   an outline, not a background change. */
.station-panel__resize {
  position: absolute; top: 0; bottom: 0; left: -7px; width: 14px;
  cursor: col-resize; touch-action: none; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 0;
}
.station-panel__resize::before {
  content: '';
  width: 8px; height: 52px;
  border-radius: 4px;
  background: var(--text-muted);
  box-shadow: 0 0 0 1px rgb(255 255 255 / .75);
  transition: height .12s, background-color .12s;
}
/* Three dots in the middle of the handle. The bare rectangle could be
   read as decoration or a piece of a frame; this texture is a common
   "grab here" marker and costs only a few pixels. Drawn via ::after,
   which sits above ::before. */
.station-panel__resize::after {
  content: '';
  /* The center is set explicitly. Without top/left/transform, the
     position would depend on where the browser places the absolutely
     positioned descendant of a flex container - Chrome centers it, but
     relying on that makes no sense when it can be stated directly. */
  position: absolute;
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 2px; height: 14px;
  background: linear-gradient(to bottom,
    var(--white) 0 2px, transparent 2px 6px,
    var(--white) 6px 8px, transparent 8px 12px,
    var(--white) 12px 14px);
  pointer-events: none;
}
.station-panel__resize:hover::before,
.station-panel__resize:focus-visible::before {
  background: var(--blue);
  height: 76px;
}
/* Ring inward, not outward: the handle is a narrow strip on the panel's
   edge and an outward ring would lie across both the map and the
   panel's content. Color is given by the global rule. */
.station-panel__resize:focus-visible { outline-offset: -2px; }

/* The bar with the close button is outside the scrolled content, so it
   always stays within reach, however long the station in the panel gets
   (long history, many channels). */
.station-panel__bar { flex: none; display: flex; justify-content: flex-end; padding: var(--space-2) var(--space-2) 0; }
/* 44 px - the close button is the only way to get out of the panel on
   mobile, and aiming at it one-handed while moving must be reliable. */
.station-panel__close { width: 44px; height: 44px; border-radius: 50%; border: 1px solid var(--gray-line);
  background: var(--white); color: var(--text); font-size: 18px; line-height: 1; cursor: pointer; }
.station-panel__close:hover { background: var(--gray-bg); }
.station-panel .detail { flex: 1 1 auto; overflow-y: auto; padding: 0 var(--space-4) var(--space-4);
  max-width: none; margin: 0; }
/* The panel's narrower area needs a smaller font than the whole
   lokalita.html page - the clamp() on .station-name computes from the
   vw of the whole screen, not the panel's width. */
.station-panel .station-name { font-size: 22px; }
.station-panel .chval--primary .chval__num { font-size: 30px; }
.station-panel .minimap { height: 200px; }

@media (max-width: 768px) {
  /* Narrow viewport: the panel slides up from the bottom like the
     filter #panel, instead of a side strip - the same pattern as the
     existing #panel further down in this file. */
  .station-panel { position: fixed; inset: auto 0 0 0; top: auto; width: auto; height: auto;
    max-height: 80vh; border-left: 0; border-top: 1px solid var(--gray-line);
    box-shadow: 0 -2px 16px rgb(0 0 0 / .25); }
  /* The panel takes the full width, so there's nothing to drag. */
  .station-panel__resize { display: none; }
}

/* Load-status notice on the standalone station card (lokalita.html).
   The card refreshes like the map and the list, and this line says as
   of what time what's visible is valid - without it, a shared link
   opened during a flood would show the original state even half an
   hour later, with nothing making it look stale. Quiet (--text-muted),
   more prominent on a refresh failure, but still not an alarm - an
   alarm is reserved for the SPA grade. */
.load-status { font-size: 13px; margin: 0 0 var(--space-3); color: var(--text-muted); }
.load-status--warn { color: var(--text); font-weight: 600; }

/* Stale channel value. The distinction must not use the SPA grade's
   color - that's reserved for the severity of the flood situation, not
   the age of the data. Hence a muted value and a prominent label below
   it, the same principle as .alert-stale (dead station), just a notch
   quieter - here it's about one channel, not the whole station. */
.chval--stale .chval__num { color: var(--text-muted); }
.chval__meta--stale { color: var(--text); font-weight: 600; }
.val--stale { color: var(--text-muted); }
.cell-age { color: var(--text-muted); font-size: 12px; white-space: nowrap; }

/* Text for screen readers only. The map and the list pages carry no
   VISIBLE h1 - a screen reader user orients on a page by headings and
   landmarks, but a "Mapa stanic" heading above a full-screen map would
   just visually eat up space. clip-path instead of display:none - a
   hidden element must stay in the tree, otherwise the reader won't
   read it either. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0;
}

/* Empty result and station not found. An empty table or map with
   just a count of "0" can be read as "nothing to worry about" - hence
   always a reason and an action to get out of that state. */
.empty-state { padding: var(--space-5) var(--space-4); text-align: center; }
.empty-state p { max-width: 480px; margin: 0 auto var(--space-3); color: var(--text); }

/* Loading. .loading covers the initial empty state before any data has
   arrived. Once a source is showing, dimming it (data-loading='true')
   while a refresh or source switch is in flight keeps the previous
   data from being mistaken for the new one - without the dim, the page
   would show the old source's content while already claiming the
   switch happened. */
.loading { padding: var(--space-5) var(--space-4); text-align: center; color: var(--text-muted); }
/* On the map these would land below the full-height map, out of sight -
   there they float over the top of the map instead, above Leaflet's
   panes and controls. */
#map ~ .loading,
#map ~ .empty-state { position: absolute; top: var(--space-4); left: 50%; transform: translateX(-50%);
  z-index: 1000; width: min(520px, calc(100% - 2 * var(--space-4)));
  background: var(--white); border: 1px solid var(--gray-line); border-radius: var(--radius); }
/* Narrow enough that the box would cover the zoom buttons - it goes
   below them. */
@media (max-width: 768px) {
  #map ~ .loading,
  #map ~ .empty-state { top: 84px; }
}
#main[data-loading='true'] .tbl,
#main[data-loading='true'] #map { opacity: .45; }

/* Keyboard sorting: the button inside <th> is what receives the click -
   <th> itself isn't focusable or activatable with Enter. The button
   looks like the original header, but it's a real control. */
.tbl th .sort-btn {
  display: inline-flex; align-items: center; gap: var(--space-1);
  background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: inherit; text-transform: inherit; letter-spacing: inherit;
}
.tbl th .sort-btn::after { content: ''; }
.tbl th[aria-sort='ascending'] .sort-btn::after { content: '↑'; }
.tbl th[aria-sort='descending'] .sort-btn::after { content: '↓'; }
.tbl th { cursor: auto; }

/* Grade next to individual channels. Smaller than the badge next to
   the station name - the station's overall state remains the main
   piece of information, this only says WHICH quantity caused it. */
.chval__level { margin-left: var(--space-2); font-size: 10px; vertical-align: middle; }

/* --- Operator and data-source logos -------------------------------------

   The logos are WHITE on a transparent background - monochrome
   variants made for the dark header of the original hladiny.cz. On a
   light background nothing would be left of them, so the dark
   background here isn't a stylistic choice.

   In the panel (map, list) --dark already holds the panel itself, so
   the bar doesn't need its own box - a hairline above it is enough, the
   same as between filter cards. The location card has no panel, so
   there the block must have its own background.

   Logo height is kept at 30 px, which is their native size: at 22 px
   "POVODI MORAVY" and "POVODI LABE" got blurry, because downsampling
   ate exactly the small text that makes the logo recognizable.

   The operator's own logo (Fiedler-Mágr) leads the block, above the
   partner bar and outside it (.credits__owner): the portal reads as
   Fiedler's own, the rest as partners. */
.credits__owner { margin: 0; }
.credits__owner a { display: inline-flex; border-radius: 2px; }
.credits__owner img { display: block; width: auto; height: 24px; }
.partners { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-3); }
/* Without a heading this bar is just a row of logos where you can't
   tell whose they are or why they're there. Same look as .eyebrow, but
   without its dash: inside a bar of logos, no further graphics are
   needed. */
.partners__title { width: 100%; margin: 0; text-transform: uppercase;
  font-size: 11px; font-weight: 600; letter-spacing: .12em; color: var(--text3); }
.partners a { display: flex; border-radius: 2px; }
.partners img { display: block; width: auto; height: 30px; }
.partners__list { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--space-2) var(--space-3); margin: 0; padding: 0; list-style: none; }

/* In the panel there's 300 px of width minus padding for the logos, so
   the bar goes into a column: the heading on its own row, the logos
   below it wrapped into rows by the grid on .partners__list (see
   below). The operator's logo gets a row of its own above it, the same
   hairline between the two as above them.

   margin-top: auto pushes the block to the panel's bottom edge when
   there's room left above it; when the filters fill the panel, the block
   simply stays after them and gets scrolled to. No height computation
   from JS - that already turned out to be fragile once in this panel
   (see the comment on the mobile .layout above). */
#panel { display: flex; flex-direction: column; }
#panel > * { flex: none; }
.credits--in-panel { margin-top: auto; }
.credits--in-panel .credits__owner,
.credits--in-panel .partners { padding: var(--space-4); border-top: 1px solid var(--dark-line); }
.credits--in-panel .partners { flex-direction: column; align-items: flex-start; gap: var(--space-3); }
/* Six logos at the panel's width come out to two rows. Free wrapping
   made it 4 + 2, and a lone second row looks like a typesetting mistake
   - three columns turn that into 3 + 3. The columns are `auto`, not
   equal width: the logos vary in width (30 to 67 px) and a forced grid
   would scatter them with empty space. */
.credits--in-panel .partners__list { display: grid; grid-template-columns: repeat(3, auto);
  justify-content: space-between; justify-items: start; align-items: center;
  gap: var(--space-3); width: 100%; }

/* The location card has no panel - there the block sits in normal flow
   at the end of the content and carries its own background. fit-content,
   so the block element doesn't stretch across the full width and look
   like a stray band. Horizontal padding is kept by the page's container
   (#detail-main already has its own padding), not by the block. */
.credits:not(.credits--in-panel) { width: fit-content; max-width: 100%;
  margin: var(--space-4) 0 var(--space-3); padding: var(--space-2) var(--space-3);
  background: var(--dark); border: 1px solid var(--dark-line); border-radius: var(--radius); }
.credits:not(.credits--in-panel) .credits__owner { padding-bottom: var(--space-2);
  margin-bottom: var(--space-2); border-bottom: 1px solid var(--dark-line); }
/* Left border and padding give the list a fixed inset from the aside's
   edge. The operator's logo stands above the partners (see the note
   above), not beside this list, so the border marks no division between
   two groups here - just the list's left edge. */
.credits:not(.credits--in-panel) .partners__list { padding-left: var(--space-3);
  border-left: 1px solid var(--dark-line); }

/* On a phone the panel is a sheet at the bottom edge, so every pixel of
   the bar takes something away from the map. Shrinking the logos here
   costs nothing: they're rasters with a native height of 30 px that get
   scaled up anyway on a display with triple the pixel density, so they
   won't get any sharper at 30 px. Six logos at 22 px fit on ONE row
   (218 px + gaps against 342 px available on a 390 px phone, and it
   also fits on 320 px), so the 3x2 grid here gives way to one row -
   61 px total instead of 108 px. But they must not disappear, they're
   a condition of being given the data. */
@media (max-width: 768px) {
  .partners img { height: 22px; }
  .credits__owner img { height: 18px; }
  .credits--in-panel .credits__owner,
  .credits--in-panel .partners { padding: var(--space-2) var(--space-4); }
  .credits--in-panel .partners { gap: var(--space-2); }
  .credits--in-panel .partners__list { display: flex; flex-wrap: wrap;
    justify-content: flex-start; gap: var(--space-2); width: auto; }
}
