/* stations.css — the list and band dial: rows, sortable header, expandable record.
   Widget: web/js/stations.js */

/* The list scrolls while the sticky, opaque header stays on top. There is deliberately no
   CSS mask: a mask forces a repaint of the whole element whenever any child changes, so the
   legitimate update of the tuned row made the entire list flicker. Without one, only the
   tuned row repaints. */
/* The card holds the list and nothing else, so it gives it all of its room: the padding of a
   widget card would push the sticky header down inside the scrollport (see below), and the
   list carries its own. */
.wcard.w-list{padding:0;}
.wcard.w-list .stations{flex:1 1 auto; min-height:0;}   /* it scrolls rather than growing */
/* Three things want the top-right corner: the slot's mark, the list's own mode selector, and
   the SCROLLBAR, which is inside the card because the list is what scrolls. The first two are
   placed against the card and stepped left of the third, whose width is measured by the script
   into `--sbw` -- the stylesheet cannot know it, it depends on the platform and on the browser's
   setting, and there is nothing to guess with since the bar is about as wide as a control.
   That is the WHOLE corner: these controls float over the column header, which runs edge to
   edge, so each one costs a column title its place and its sort click. Two fit because a glyph
   is narrow and the far-right columns are the least read; a third does not, and the discovery
   scans went to the spectrum for that reason. */
.wcard.w-list .wnav{right:calc(32px + var(--sbw, 0px));}
.wcard.w-list #stMode{right:calc(8px + var(--sbw, 0px));}
/* No padding at the top, and that is load-bearing: a sticky element cannot leave its
   containing block, which is this element's CONTENT box. Any padding here would hold the
   header that far below the top of the scrollport while rows kept scrolling through the gap,
   showing a sliver of the list above the header. The header carries its own top padding. */
/* Scroll anchoring off. The browser normally watches a scrolling box for content appearing or
   disappearing above the scroll position and compensates, so the reading stays put -- which is
   the right default for a page of text and the wrong one here twice over. This list decides its
   own scroll position: the tuned row is centred and the list moves around it, so a second party
   correcting that position fights the dial. And the correction is recomputed on every mutation,
   while a sort by a live column moves dozens of rows per pass. */
.stations{padding:0 6px 6px; overflow:auto; overflow-anchor:none;
  /* Column widths: a single source for all three row kinds, so header, row and record
     cannot diverge. Written from the script (defaults, then dragging) and persisted; the
     values here are only a bootstrap before the first application. */
  --c1:22px; --c2:48px; --c3:98px; --c4:58px; --c5:26px; --c6:96px;
  --c7:132px; --c8:96px; --c9:52px; --c10:46px; --c11:42px; --c12:50px;
  /* The template lists the VISIBLE columns and is rewritten whenever that set changes. This
     bootstrap only has to hold for the frame before the script runs: without it the template
     would resolve to nothing and the row would collapse into a single column. */
  --tmpl:var(--c1) var(--c2) var(--c3) var(--c4) var(--c5) var(--c6);}
/* Aligned columns, as in a measurement table. All fixed and packed to the left, with the
   empty space pushed to the end. No elastic column: a station name is eight characters at
   most, and giving it a flexible width dug a hole between the name and the identity code.
   Stereo has its own column, and the measurement fits in one: a fixed-width bar so rows
   compare at a glance, its value, and the unit attached to the value.
   The last columns are the transmitter, which the backend resolves for every identified
   row. That is analysis, so it sits on the right, after the measurement, and it finally
   occupies the empty end.
   Every width is adjustable with the mouse through a header handle. */
/* Uniform typography: one size and one family for all the list data. It once carried eight
   sizes and two families, with each cell negotiating its own scale. A measurement table
   reads in columns: alignment is what creates hierarchy, not size. What distinguishes a row
   is now colour and weight alone. */
/* A table of measurements is read along a line. Every cell gets the SAME LINE BOX -- one
   line-height, in pixels so a smaller unit cannot grow it -- and equal boxes align by
   themselves. Baseline alignment does the same thing by asking the browser to measure thirteen
   cells and place twelve of them against the lowest, which is what made this list expensive. */
/* Off-screen rows are skipped. The list holds the whole plan -- a couple of hundred rows of a
   dozen cells each -- while a score of them are visible, and every structural operation was
   being paid on the whole of it: creating a row, moving one and rewriting one were measured at
   1.1, 0.19 and 0.26 ms apiece, three unrelated operations at the same price, which is the
   signature of a cost that follows the NUMBER OF NODES rather than the work asked for.
   The placeholder height is the one measured on a real row and published by the script, so the
   scrollport is the right height before anything is rendered; `auto` then keeps each row's last
   rendered size, which is what makes an expanded row keep its own. Without a truthful height
   here the dial would centre against a guess. */
.strow{display:grid; grid-template-columns:var(--tmpl); align-items:center; line-height:17px;
       gap:9px; padding:4px 8px; border-radius:5px; cursor:pointer;
       font-family:var(--mono); font-size:12px;
       content-visibility:auto; contain-intrinsic-size:auto var(--rowH, 26px);}
.strow .idx{color:var(--dim2); text-align:right; font-variant-numeric:tabular-nums;}
/* Column header: sticky and clickable to sort. Same grid as a row, so they align. */
.sthead{display:grid; grid-template-columns:var(--tmpl); gap:9px;
        padding:3px 8px 5px; position:sticky; top:0; z-index:2; border-bottom:1px solid var(--edge);
        background:linear-gradient(180deg,var(--panel2),var(--panel));
        font-size:9px; letter-spacing:.1em; color:var(--dim2); text-transform:uppercase;}
.sthead span{cursor:pointer; user-select:none; white-space:nowrap; position:relative;}
/* Resize handle: invisible at rest, a hairline under the cursor. It is a control, not
   decoration, and it overlaps the gutter, so it consumes no data width. */
.colres{position:absolute; top:-3px; right:-7px; width:10px; height:calc(100% + 6px);
        cursor:col-resize; z-index:3;}
.colres:hover{background:linear-gradient(90deg,transparent 0 4px,var(--teal) 4px 5px,transparent 5px);}
.sthead span:hover{color:var(--ink);}
.sthead span.act{color:var(--teal);}
.sthead .idx{text-align:right;}
/* Band mode: the position carries the information and no column sorts, so the header says so. */
.sthead.nosort span{cursor:default;}
.sthead.nosort span:hover{color:var(--dim2);}
/* The mode selector takes the shared panel corner toggle as it is, positioned against the CARD
   like every other widget's -- so nothing is defined here at all. It used to be anchored to the
   header, and that was the defect: a sticky header is a positioned ancestor, but its right edge
   sits inside the card by the width of the scrollbar, so the control drifted right and ended up
   underneath the slot's corner mark. Hanging off the card also keeps it in view while the list
   scrolls, which was the reason for the original anchoring. */

/* Which columns are shown is the operator's choice, kept by position like the widths. The
   template is built from the visible ones alone, so a hidden column leaves no gap behind it,
   and the cells go through one class on the panel rather than a style written on every cell
   of every row -- 206 rows in band mode.
   Written out, one rule per column: a loop would have to live in JavaScript and touch the
   DOM on every toggle, where this is a single class swap. */
.stations.hc3 > .sthead > span:nth-child(3){display:none;}
.stations.hc4 > .sthead > span:nth-child(4){display:none;}
.stations.hc5 > .sthead > span:nth-child(5){display:none;}
.stations.hc6 > .sthead > span:nth-child(6){display:none;}
.stations.hc7 > .sthead > span:nth-child(7){display:none;}
.stations.hc8 > .sthead > span:nth-child(8){display:none;}
.stations.hc9 > .sthead > span:nth-child(9){display:none;}
.stations.hc10 > .sthead > span:nth-child(10){display:none;}
.stations.hc11 > .sthead > span:nth-child(11){display:none;}
.stations.hc12 > .sthead > span:nth-child(12){display:none;}
.stations.hc13 > .sthead > span:nth-child(13){display:none;}

.strow:hover{background:#13202b;}
/* The tuned station is the centre of the dial: emphasised and one notch stronger. */
.strow.on{background:#15222c; box-shadow:inset 3px 0 0 var(--amber);}
.strow.on .f{color:var(--amber); font-weight:600;}
.strow.on .ps{color:#f3f8fb; font-weight:600;}
.strow.on .pi{color:#cfe0ea;}
/* Band mode: a channel of the plan that nothing occupies. It keeps its place, which is the
   point of a band ruler, but recedes -- the frequency and its measured level place it. */
.strow.empty .f{color:var(--dim2);}
.strow.empty .lvl{opacity:.55;}
.strow.transient{font-style:italic;}   /* the live dial position, not a catalogued station */
.strow.transient .idx{color:var(--amber);}
.strow .f{color:#dbe6ee;}
.strow .ps{color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
/* The progressively decoded name of the tuned row uses the same rendering as the listening
   panel; only the colours are redeclared here, the panel's being scoped to its own tile. */
.strow .ps .psc{white-space:pre;}
.strow .ps .psc.dec{color:#eaf7f2;}
.strow .ps .psc.hint{color:#93a6b4;}
.strow .ps.inh{opacity:.5; font-style:italic;}   /* name inherited from another frequency */
/* One doubt, marked wherever it reaches. The tuner never granted this code its full
   confidence: the code is shown with a reservation rather than withheld, and so is a name
   that is only a translation of that code. Generated content, so it survives the text being
   rewritten on every refresh and costs no element. A name decoded off the air carries no
   such mark -- it does not come from the code. */
.strow .piv.q::after, .strow .ps.q::after{content:"?"; color:var(--dim); margin-left:2px;}
/* Qualified with no identity: a confirmed peak or a measured width, but nothing naming it.
   The dash says "there is something here, we do not know what"; an empty cell would read as
   a bug. */
.strow.anon .ps{color:var(--dim2);}
/* Refuted by measurement: the sweep saw a peak and the tune denied it. Visible in band mode
   only, and struck through, because it is a diagnostic tool. */
.strow.refuted .f, .strow.refuted .ps{text-decoration:line-through; color:var(--dim2);}
.strow.refuted .lvl{color:var(--dim2); opacity:.5;}
/* Stereo: the letters alone, not a filled chip. The listening panel shows one flag on one
   station and can afford a solid badge; a list stacks hundreds of them down a column, where
   the same badge becomes a wall of colour competing with the measurement. Density is the
   reason the two differ, and the meaning is unchanged -- absent when the pilot was never
   seen, which is not "mono" but "no proof". */
.strow .stf{font-style:normal; letter-spacing:.08em; font-weight:400; color:#3f7d77;}
.strow.on .stf{color:var(--teal);}
.strow .stf:empty{display:none;}
.strow .pi{color:var(--dim); display:flex; align-items:center; gap:5px;}
/* Channel record: several occupants on one frequency. The counter appears only from two,
   since a single station is the normal case. It expands rather than tunes, hence the wider
   click target and the distinct cursor. */
/* A length, like the row's own: a ratio recomputes against this element's font-size. */
.strow .stn{font-style:normal; line-height:15px; padding:0 4px; border-radius:2px;
            color:var(--dim); background:none; border:1px solid var(--edge);
            font-variant-numeric:tabular-nums; cursor:pointer;}
.strow .stn:hover{color:var(--ink); border-color:var(--dim);}
.strow .stn.open{color:var(--teal); border-color:var(--teal);}   /* a state, not decoration */
/* The expanded record is a full-width band beneath the cells, so the grid keeps its columns. */
.stsub{grid-column:1/-1; margin:4px 0 1px; padding:3px 0 1px; border-top:1px solid var(--edge);}
/* The record reuses exactly the row's columns: name under name, identity under identity,
   level under level. It once had a grid of its own, aligned with nothing -- a table inside a
   table. The index column serves as indentation and the frequency column carries the date
   last heard. */
.ssrow{display:grid; grid-template-columns:var(--tmpl); gap:9px;
       align-items:center; padding:2px 8px; color:var(--dim);
       font-variant-numeric:tabular-nums;}
.ssrow.on{color:var(--ink);}                  /* the current occupant of the row */
.ssrow.on .sspi{color:var(--amber);}
.ssrow .ssps{overflow:hidden;
             text-overflow:ellipsis; white-space:nowrap;}
/* The catch count annotates the name in its own cell: it informs the decision without
   taking it. No day counter, because the table is the live view and not a logbook. */
.ssrow .ssrec{font-style:normal; color:var(--dim2); margin-left:8px;}
.ssrow .sslvl{text-align:right;}   /* aligned under the parent row's value */
/* The level cell had the class `meter`, which the listening panel also uses for its continuous
   -trace instruments. Sharing a name across two sheets is a collision waiting for whichever is
   loaded last, and it went off: `listen.css` says `.meter{margin:0 0 8px}`, so the cell carried
   a bottom margin it never asked for. Centred as a grid item it is the MARGIN box that is
   centred, so the level and its unit sat four pixels high, and the row was eight pixels taller
   than the others.
   Scoping was the wrong answer -- it protects the other sheet from this one and not the reverse.
   Two different things do not share a name. `sigc` follows `stc`, the stereo cell beside it. */
/* Packed to the RIGHT, like the header above it and like every other measurement column. It was
   packed left inside a cell wide enough for the transmitter block, so the figures sat some forty
   pixels from the word they belong under -- and the wider the operator made the column, the
   further they drifted. The digits keep their own fixed box inside, so they still line up with
   each other whatever the unit. */
/* Not a flex box: the figure and its unit are two pieces of TEXT on one line, and inline layout
   already puts text on a shared baseline, for nothing. Made flex items they each got their own
   line box and had to be aligned back together, which is the work this cell does not need. */
.strow .sigc{text-align:right; min-width:0;}
.strow .lvl{display:inline-block; width:22px; text-align:right; color:var(--dim);
            font-variant-numeric:tabular-nums; font-weight:400;}
/* The level is read as a figure and placed by its shade. The shade codes DECIBELS ABOVE THE
   MEASURED NOISE FLOOR, not absolute dBuV: what counts is how far a carrier stands out of
   the noise where it sits, and the floor depends on the antenna and the site.

   A continuous ramp, written directly in oklch so all three dimensions move at once and
   perceptually: lightness, chroma and hue. Mixing two fixed colours instead left the middle
   of the scale grey, because the shortest path between them in a perceptual space goes
   through low chroma -- a luminance ramp, not a colour one.

   Continuous because a stepped scale makes a carrier hovering near a boundary flip between
   two shades without its signal really changing.

   Chroma is what carries the reading: buried in the noise is nearly grey, and colour comes
   with the signal, so a strong carrier is vivid rather than merely pale. The hue turns from
   blue to green along the way -- the idiom of a spectrogram, not of a car radio, which is a
   red-to-green verdict on quality and not a scale of level. It stops short of yellow, which
   belongs to the tuned row. */
.strow .lvl.sig{
  color:oklch(calc(45% + var(--sig, 0) * 43%)      /* dim to bright */
              calc(0.02 + var(--sig, 0) * 0.15)    /* grey to vivid */
              calc(250 - var(--sig, 0) * 95));     /* blue through cyan to green */
}
/* The unit is distinguished by COLOUR ALONE. It is written in `small`, which the browser makes
   smaller on its own, and that was the last thing in a row whose text differed from its
   neighbours' -- the one cell that could not be made to sit on the same line as the rest. One
   family, one size, one line-height for every piece of text in a row: there is then nothing left
   to align, which is cheaper than any way of aligning it.
   Kept on one line, because the numeric columns are narrow and a unit wrapping under its figure
   would make that row taller than the others. */
.strow .unit{color:var(--dim2); margin-left:3px;
             font-size:inherit; line-height:inherit; white-space:nowrap;}
/* Transmitter columns. Numeric ones are right-aligned with tabular figures and the unit
   attached, the same idiom as the signal: a column of measurements reads as a column. */
.strow .num, .sthead .num{text-align:right; font-variant-numeric:tabular-nums;}
.strow .num{color:var(--dim);}
.strow .site{display:flex; align-items:center; gap:5px; min-width:0; color:var(--dim);}
.strow .city, .strow .pol{color:var(--dim); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.strow .site .gnm{font-weight:400; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.strow .site .gfl{font-style:normal; line-height:1;}
/* The approximation marker means several sites share the identity code and cannot be told
   apart. Same convention as the transmitter widget: nothing when the site is certain. */
.strow .site .gamb{font-style:normal; color:var(--amber);}
.strow.on .site, .strow.on .num, .strow.on .city, .strow.on .pol{color:var(--ink);}
/* The tuned row keeps its measured shade and gains weight only. Repainting it amber would
   erase the one reading you are actually looking at; the row already says where you are with
   its background, its edge and its frequency. */
.strow.on .lvl{font-weight:600;}
.strow.on .unit{color:var(--dim);}
