/* listen.css — the listening panel: identity tiles, text line, continuous-trace meters,
   filter, transmitter and RF impairments.
   Widget: web/js/listen.js */

/* The listening panel is a widget card: the box comes from `.wcard`, and only its own
   padding is stated here. Space distribution is deliberately not spread between blocks: on a
   height deficit that behaved like a top alignment and pushed the last block outside the
   frame. Every block keeps its reserved height and the free room stays at the bottom, which
   is the room the boundary above gives back to the trace. */
.wcard.w-listen{padding:6px 16px 8px;}
/* The text line closes the card, on its own padding. Its bottom margin was there to separate it
   from the widget pair that used to sit below; there is nothing under it now, so the margin only
   deepens the floor. Named rather than reached with `:last-child`, which is the corner mark: the
   slot appends it after the widget's markup. */
.wcard.w-listen > .rtfull{margin-bottom:0;}
/* Floor on the card that needs it, like the time graph's. Every block above has a reserved
   height, so their sum is exactly the point below which the panel stops being a reading: the
   identity would be cut in half and the text line would go. The card is also the slot, so this
   has to outweigh the `min-height: 0` every row item carries.
   It is the SUM, not a round number: any slack in it is room the trace and the pair never get
   back, since the floor wins over the share the operator dragged.
   The share is the same floor for a short screen, where a pixel floor stops protecting a
   reading and starts pushing the page out of a viewport that cannot scroll. */
[data-slot].wcard.w-listen{min-height:min(146px, calc(20 * var(--vh, 1vh)));}
/* Identity row: identity code, frequency, name and programme type on one line. The grid
   sizes by position rather than by class, so the widths follow that same order. */
.idrow{display:grid; grid-template-columns:0.7fr 1fr 1.3fr 1fr; gap:7px; margin-bottom:8px;}
.tile{background:#0c1218; border:1px solid var(--edge); border-radius:2px; padding:6px 10px; min-width:0;}
.tile .tk{font-size:9px; letter-spacing:.16em; text-transform:uppercase; color:var(--dim);}
/* Height reserved per tile, so an empty field does not make the row tremble. */
.tile .tv{font-family:var(--mono); color:var(--ink); margin-top:3px; line-height:1;
          white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.tile .tv.stale{opacity:.55;}   /* already seen but no longer received: dimmed, kept */
.tile.freq{background:linear-gradient(180deg,#101820,#0b1219);}
/* The readout between its two steps, centred in the tile. This is the one tile whose value
   has controls beside it, so the row is stated here and not on `.tile`. */
.tile.freq .tv{min-height:30px; display:flex; align-items:center; justify-content:center;}
/* The channel step, flanking the digits at their own height and with no box of its own: it is
   part of the reading, not a keypad set down next to it. Quiet at rest, since an instrument
   shows its measurement first and its controls second. The width is fixed so the digits sit
   at the same place whether or not the pointer is over one. */
.tile.freq .fstep{flex:0 0 auto; width:22px; text-align:center; cursor:pointer;
                  font-size:26px; line-height:30px; color:var(--dim2); user-select:none;}
.tile.freq .fstep:hover{color:var(--teal);}
.tile.freq .fstep:active{transform:translateY(1px);}
/* Identity, frequency and name share one type size: three faces of a single identity, with
   no rank between them. */
.tile.freq .bigfreq b{font-size:30px;}
.tile.freq .bigfreq i{font-size:12px; margin-left:3px;}
.tile.pi .tv{font-size:30px; color:var(--teal); min-width:4.5ch; min-height:30px;}
/* The country code reserves no space: it comes after the identity code in a left-aligned
   tile, so its arrival shifts nothing, and it is absent most of the time. Reserving would
   have dug a permanent hole for a field that only lights up on distant reception. */
/* The tuner's confidence marks share the code's type size, since they are part of what the
   tuner writes, but are dimmed: the code reads first, the reservation second. No space is
   reserved, so a confident code leaves no hole. */
.tile.pi .piq{color:var(--dim); letter-spacing:-.04em;}
.tile.ps{background:linear-gradient(180deg,#101a16,#0c1512); border-color:#1c3a30;}
.tile.ps .tv{font-size:30px; color:#eaf7f2; letter-spacing:1px; font-weight:600; min-height:30px;}
.tile.ps .tk{color:var(--teal);}
/* The level tile shares that same size and carries the decimal value. Its minimum width is
   in character units, so short and long values occupy the same space and the figure does
   not dance between measurements. */
.tile.sig .tv{font-size:30px; min-height:30px;}
.tile.sig .tv #vSig{display:inline-block; min-width:4.5ch;}
.tile.sig .tv small{font-size:11px; color:var(--dim); margin-left:4px;}
/* Banner: flags, programme type and the signal meter, on the title line. In flow rather
   than absolute, because the meter needs a real width. It starts at the panel edge: the
   panel carries no caption, so the measurements begin where the reading does. */
.lhead{display:flex; align-items:center; gap:12px; margin-bottom:8px;
       padding-bottom:13px;}   /* reserves the height of the ruler, taken out of flow below */
/* The signal meter no longer stretches across the full width: a very long track for a
   modest range gives the level a visual weight it does not have in the reading. Bounded and
   left-aligned, right after the flags, so the row reads in one stroke and the empty space
   is pushed to the end. */
.lhead .sigrow{flex:0 1 auto; min-width:0;}
/* Alignment is on the bar, not on the bar-plus-ruler block. Leaving the ruler in flow made
   the block taller, so the chips centred on that and their axis fell below the bar's. With
   the ruler absolutely positioned under the bar, everything shares one axis. */
.lhead .nbody{position:relative; width:340px; max-width:100%;}
.lhead .ruler{position:absolute; left:0; right:0; top:100%;}
/* The programme type is a labelled chip built on the flag template, so the label stays
   readable even without a value. The value has a variable length, so a fixed width plus an
   ellipsis keeps the chip from ever resizing and the meter that follows from moving. */
.flag.pty{display:inline-flex; align-items:center; gap:5px; flex:0 0 auto;}
.flag.pty b{display:inline-block; width:92px; font-weight:400; color:var(--ink);
            overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
#vPs .psc{white-space:pre;}                        /* character cells keep a stable width */
#vPs .psc.dec{color:#eaf7f2;}                      /* really decoded segment */
/* Guessed name from the registry: a rank below a decoded character, and no more. It is a
   WAITING state -- the progressive name has not arrived yet -- so it must not borrow the
   look of a station whose RDS has dropped, which is a different fact entirely. A solid
   colour rather than an opacity: opacity multiplies with the one the stale value carries,
   and the two together took the guess down to nothing. */
#vPs .psc.hint{color:#93a6b4;}
/* Activity pixel: pulses on each reception of its field, driven by the rendering. */
.apx{display:inline-block; width:4px; height:4px; border-radius:50%; background:var(--green);
     opacity:0; vertical-align:middle; box-shadow:none; will-change:opacity;}
     /* Promoted to its own compositor layer, so its blinking composites instead of
        repainting the label around it. */
.flags{display:flex; gap:6px; align-items:center;}
/* Chip groups never give up width: when space runs short the meter track compresses, not
   the flags -- a truncated flag no longer reads. */
.lhead .flags{flex:0 0 auto;}
/* The banner reads as two blocks: carrier, then RDS subcarrier. A discreet rule marks the
   boundary, and it is the only hint that the right-hand bar measures RDS and not signal. */
.lhead .rdsf{margin-left:2px; padding-left:14px; border-left:1px solid #182430;}
.flag{font-size:10px; font-family:var(--mono); color:var(--dim2);
      border:1px solid #1f2a33; border-radius:2px; padding:2px 6px; letter-spacing:.08em;}
.flag.on{color:#06231c; background:var(--teal); border-color:var(--teal); font-weight:700;}
/* When RDS is lost, the flags it carried are kept but dimmed, the same convention as the
   identity: the last known state is shown without asserting it is current. */
.flag.stale{opacity:.4;}
.flag.amber.on{background:var(--amber); border-color:var(--amber); color:#241c02;}
/* The RDS flag is an ordinary flag but keeps its own colour when active. Colour logic:
   green means the data is present, the trace colour is the default, amber is an alert. */
.flag.rds.on{color:#041a12; background:var(--green); border-color:var(--green);}

/* Text line and RDS health. One single line: label, text, rate. A separate header cost a
   whole line for two small labels while the text itself did not move.
   Aligned to the top rather than to the baseline: baseline alignment depends on content, so
   an empty text has no line box, the layout falls back to the bottom of its reserved height
   and the label dives, then rises again as soon as text arrives. Here all three start from
   the same top and share the first line's height, so the position is fixed whether the text
   is present or not, on one line or two. */
.rt{display:flex; align-items:flex-start; gap:12px;
    margin:0 0 8px; padding:5px 11px; background:#0b1218;
    border:1px solid #18222c; border-radius:2px; font-family:var(--mono);
    color:#bfe9df; line-height:1.4; letter-spacing:.02em;}
.rt .ptag,.rt .ber{flex:0 0 auto; font-size:10px; line-height:17px;}   /* the first line's height */
/* The A/B lamps: a fixed pair on the label side, never a character in front of the text, which
   would read as its first letter. They are FLAGS, the chip this panel already lights for every
   other state, so a new indicator does not arrive with a look of its own. Both are always
   drawn, so the block keeps ONE width and the text never shifts sideways -- and a message that
   does not exist is an unlit lamp, which is a reading, where a gap would be nothing at all.
   The height is pinned to the first line's, or a chip taller than the label would grow the box
   it sits in. */
.rt .rtlamps{flex:0 0 auto; display:flex; gap:6px;}
.rt .rtlamp{padding:2px 5px; line-height:11px; letter-spacing:0; font-weight:700;}
/* The two messages are worth the same, so the two lamps are two hues of ONE weight -- neither
   may read as a warning, since neither is a fault. A keeps the colour every lit flag in this
   panel already uses, so the pair adds one hue to the instrument rather than two. The text
   takes the tone of the lamp that is lit: the pairing IS the legend, and it needs no reading. */
#vRt{color:#bfe9df;}
.rt.rtb #vRt{color:#c3cdf7;}
/* While the next message covers the one on display, both are on the line at once and each
   character wears the colour of the message it belongs to -- the same two hues as the lamps.
   The covering is then readable as it happens, which on a weak signal is the only sign that
   anything is being decoded at all, and it needs no legend: the lamps are the legend. */
#vRt .rtc.a{color:#bfe9df;}
#vRt .rtc.b{color:#c3cdf7;}
/* Assembling: a message exists under this flag but is not on screen yet, so the lamp is armed
   -- outlined in its colour, unlit -- rather than dark, which would deny it exists. */
.rt .rtlamp.la.wait{color:var(--teal); border-color:var(--teal);}
.rt .rtlamp.lb.wait{color:var(--blue); border-color:var(--blue);}
/* Lit, for the message on screen and for the flash of a group that has just fed one. Same
   rule for both, because it is the same statement: this message is what the receiver holds
   right now. Declared AFTER the armed state, which it overrides at equal weight. */
.rt .rtlamp.la.on,.rt .rtlamp.la.hit{color:#06231c; background:var(--teal); border-color:var(--teal);}
.rt .rtlamp.lb.on,.rt .rtlamp.lb.hit{color:#03142b; background:var(--blue); border-color:var(--blue);}
.rt .ptag{color:var(--dim); letter-spacing:.12em;}
.rt .ber{margin-left:auto; color:var(--green);}
/* Reserved height, so a short or long text does not make the area tremble. ONE line, which is
   the whole of what can arrive: the field is 64 characters at most and the panel is two modules
   wide, where 64 monospace characters take about 460 px of the 780 available. Reserving a second
   line dug a permanent hole for a wrap that cannot happen. It only can below roughly 570 px of
   viewport, where the layout is stacked and nothing sits under this to be pushed. */
/* The blanks are BROADCAST content -- padding, centring, the gap the station left between
   two words -- so they are kept as they arrived, and they are kept the same whether the line
   is one string or one span per character. Collapsed on one path and preserved on the other,
   the text jumped sideways the instant a message started covering another, which read as a
   tremor in the instrument. Wrapping survives, for the narrow layout where the line can. */
#vRt{flex:1 1 auto; min-width:0; font-size:12px; min-height:17px; line-height:1.4; display:block;
     white-space:pre-wrap;}
/* Fixed-width overmodulation indicator, so its appearance does not shift the value. */
#vOvm{display:inline-block; width:16px; text-align:center;}

/* Each slot carries the class of the widget occupying it, set at mount time, so a widget's
   own styles attach without depending on its position. The prefix avoids colliding with the
   widget's internal classes. */
/* A panel is a CONTAINER of slots, not a slot: it stacks as many cards as the markup puts in
   it, and every container has the same box, because a widget must not look different for
   having moved.

   The slots SHARE the panel's height, and the share of each is dragged on the boundary between
   two of them. The value is therefore continuous and lives in a style written by the script; the
   attribute here carries nothing but the default, and no height is ever frozen in pixels.

   A column of shares rather than a grid of equal rows, because a slot must be able to take NO
   share at all: an empty one that hands its room to a neighbour still has to exist, to be
   clickable, and equal rows would give it a full one. */
.side{display:flex; flex-direction:column; gap:7px; padding:6px; min-height:0;}
/* A ROW ITEM: what takes a share of a rows container's height. A slot, a container of slots
   stacked among them, or the whole middle row -- the page itself is such a container, which is
   why this hangs off `data-slots="rows"` and not off the panels alone.
   A share and not a content-driven height, and that is load-bearing: a card whose canvas grows
   to fill what it is given changes the height it was just measured in, so a content-sized item
   makes the resize observer fire on its own output and the page loops. The share decides the
   height first, which breaks the cycle. */
[data-slots="rows"] > [data-slot]{min-height:0; flex:1 1 0;}
/* An empty slot is still a PLACE, not a hole: it keeps an outline and its corner mark, which
   is the only way a widget can be put back into it. Nothing to see, so the mark is what there
   is to see, and it does not wait for a hover to appear. */
.wslot-empty{position:relative; background:#090d11; border:1px dashed #16212a; border-radius:2px;}
.wslot-empty .wnav{opacity:.5;}
.wslot-empty:hover .wnav{opacity:1;}
/* Folded: the script gives it no share, so only its own height is stated here. */
[data-slots="rows"] > [data-slot].wslot-folded{flex-basis:auto; height:22px;}
/* An empty CONTAINER shows nothing at all. An empty slot inside a panel is a place you can see
   and fill, and its mark is the way in; a whole region of the page with nothing in it is just a
   bar across the screen. The way back into it is the `Move` entry on the widget that belongs
   there, and the script keeps the last visible place of a family from collapsing. */
[data-slots="rows"] > [data-slots].wslot-folded{display:none;}
/* Except while something of its family is mounted nowhere: then it is not noise, it is the only
   place left to put it, and a place nobody can point at is a trap. Shown as the same bar an
   empty slot folds to, so its slots' marks are reachable and nothing else changes. */
[data-slots="rows"] > [data-slots].wslot-folded.wslot-hint{display:flex; flex-basis:auto; height:22px;}
.wslot-folded .wnav{top:1px;}   /* the bar is the height of the mark, so it is centred in it */

/* The boundary between two stacked slots, which is where their shared height is traded. It sits
   IN the gap and takes none of it: zero height, and one of the two gaps it introduced pulled
   back, so adding a handle does not move anything. Its grab area is deliberately taller than the
   gap, since a seven-pixel target is not one. */
.wgrip{flex:0 0 auto; height:0; margin-top:-7px; position:relative; z-index:4;}
.wgrip::after{content:""; position:absolute; left:0; right:0; top:-4px; height:15px;
              cursor:row-resize;}
.wgrip i{position:absolute; top:-7px; left:50%; transform:translateX(-50%); font-style:normal;
         font-size:11px; line-height:14px; color:var(--dim2); opacity:0; pointer-events:none;
         transition:opacity .15s ease; background:var(--panel2); padding:0 6px; border-radius:3px;}
.side:hover .wgrip i{opacity:1;}
.wgrip:hover i{color:var(--teal); opacity:1;}
/* Bitten onto another line of the page: said while the gesture is still under the finger, since
   after the release there is nothing left to explain. */
.wgrip.snap i{color:var(--teal); opacity:1; box-shadow:0 0 0 1px var(--teal);}
/* Touch, where there is no hover: the handle states itself, faded. */
@media (hover:none){ .wgrip i{opacity:.45;} }
/* A PAIR: a container whose slots share a WIDTH instead of a height. Two modules cut in two,
   which is the only place in the page a one-module widget fits in the middle column. The
   columns are strictly equal, because these are interchangeable slots and not two boxes each
   tailored to its content: a widget that changes side must neither grow nor shrink, otherwise
   the swap reads as a layout jump. Same gap as the container it sits in, so its boundary reads
   like every other. */
.ez2{display:grid; grid-template-columns:1fr 1fr; gap:7px; align-items:stretch; min-height:0;}

/* A widget host, wherever it sits: a card in a lateral panel and one in the middle column are
   the same object, so a widget that moves finds the same box. */
.wcard{margin:0; display:flex; flex-direction:column; background:#0b1116; border:1px solid #141e22;
       border-radius:2px; padding:9px 11px; min-height:0;   /* the card can shrink without its canvas overflowing */
       overflow:hidden;}
       /* A card never paints outside its box. Dragged smaller than its content it CLIPS, where
          overflowing would spill it over the very neighbour whose room it just gave up -- two
          instruments printed on top of each other, which reads as a fault and not as a limit. */
.wcard.w-geo .geo{flex:1; margin-top:0;}               /* the transmitter fills its card */
.wcard.w-filter canvas{flex:1; height:auto; min-height:48px; margin-top:6px;}   /* scales instead of clipping, with a readable floor */
/* Full-width text above both cards, so it fits on one line far more often. */
.rtfull{margin:0 0 8px;}
/* Continuous-trace meters. */
.meter{margin:0 0 8px; font-family:var(--mono);}
.meter .mh{display:flex; align-items:baseline; gap:8px; margin-bottom:3px;}
.meter .mv{margin-left:auto; color:var(--ink); font-size:14px;}
.meter .mv small{color:var(--dim); font-size:10px;}
.meter .mpk{color:var(--amber); font-size:11px;}
/* The numeric reminder attached to the bar is deliberately discreet: the reading happens in
   the tile, with its decimal. Here the integer is enough to place the bar on its scale. */
.lvlbig{margin-left:auto; color:var(--dim); font-size:12px; line-height:1;}
.lvlbig small{font-size:9px; color:var(--dim2); margin-left:2px;}
.lvlbig .pk{font-size:11px; color:var(--green); font-style:normal; margin-left:8px;}
/* Signal row inside the banner: an extensible bar on the left, the reminder on the right. */
.sigrow{display:flex; align-items:center; gap:12px;}
.sigrow .nbody{flex:1; min-width:0;}
/* Transmitter: the resolved station, sole occupant of its card. */
/* Fixed-column grid: target, text, logo. Every slot has a frozen width, filled or not, so
   the group always occupies the same width and the target never moves, station resolved or
   not. Centred both ways inside the card. The flag lives on the city line, next to the
   place it qualifies.
   The TARGET LEADS and the logo closes the row, because the logo is the slot that empties
   on its own: a station without one, or nothing identified at all. Held first, its absence
   dragged the target sideways and let it grow into the freed column — the instrument
   changing size according to whether a brand image exists, which is not a measurement. */
.geo{display:grid; grid-template-columns:minmax(104px, 172px) minmax(0, 1fr) auto; gap:16px;
     align-content:center; align-items:center; margin-top:8px; min-height:56px; min-width:0;}
/* At rest the slot is kept, with the empty logo box and the rose visible and the rest
   hidden, so nothing collapses and what sits above never drops. */
.geo.empty .gtxt, .geo.empty .gdist{visibility:hidden;}
.gslot{flex:0 0 auto; width:88px; height:66px; border-radius:5px; background:#0c1218;   /* fixed logo slot */
  display:flex; align-items:center; justify-content:center; overflow:hidden;}
.glogo{max-width:100%; max-height:100%; object-fit:contain;}
.glogo[hidden]{display:none;}   /* no logo: the slot stays empty and keeps its footprint */
.gcompassbox{display:flex; flex-direction:column; align-items:center; justify-content:center;
             gap:3px; flex:0 0 auto; min-height:0; max-height:100%;
             position:relative;}   /* the hover hint is placed against this box */
/* The target follows the width its column is given, which the grid grows into the card
   rather than leaving as margin. Square by ratio, never by a frozen pair of pixel values,
   so it cannot go oval and cannot overflow the column on a narrow panel. */
.gcompass{width:100%; height:auto; aspect-ratio:1; flex:0 1 auto; min-height:0;
          max-height:100%;}                      /* a short, wide card shrinks it rather than clipping */
/* At rest, a faded placeholder. The state changes instantly: fading in over 400 ms would
   mean the tile still looks empty when the receiver already knows where it is. */
.geo.empty .gcompass{opacity:.3;}
.gring{fill:none; stroke:#1c2a38; stroke-width:1.4;}
.gringi{stroke:#16212c; stroke-width:1;}     /* inner range rings: a scale, not a frame */
.gtick{stroke:#2a3a4a; stroke-width:1.1; stroke-linecap:round;}   /* cardinal direction ticks */
.gtickn{stroke:#5a6b7c; stroke-width:1.5;}                        /* north is the reference */
.gme{stroke:var(--dim); stroke-width:1.1; stroke-linecap:round; fill:none;}   /* the receiver */
/* A site on the target. Amber is the resolved one, as everywhere else in the page; the
   others carry the measurement colour, their fill following plausibility -- the same
   language the station list uses for signal, so the page says it once. */
.gmark{fill:var(--teal); stroke:var(--teal); stroke-opacity:.55; stroke-width:.9;}
.gmark.gwin{fill:var(--amber); stroke:none;}
.gspoke{stroke:var(--amber); stroke-width:1.3; opacity:.55;}
/* Nothing was identified: the tile shows what the catalogue EXPECTS on this channel. It
   must not be mistakable for a reading, so it drops the two marks of one — the amber and
   the logo — and recedes as a whole. The head of the list is still the largest mark, so
   the tile and the target name the same site, but it is hollow: an outline, not a fill. */
.geo.possible{opacity:.82;}
/* The slot keeps its footprint, emptied: dropping it out of the layout would hand its width
   to the target and the tile would change geometry between modes. Same instrument, same
   size, whether the channel was identified or not. */
.geo.possible .gslot{visibility:hidden;}
.gmark.gtop{fill:none; stroke:var(--teal); stroke-opacity:1; stroke-width:1.6;}
.geo.possible .gname #gSite{color:var(--dim);}     /* a name proposed, not established */
.geo.possible .gmeta #gPi{color:var(--dim2); font-weight:400;}
/* The scope of the claim leads the list: six of eight and six of two hundred are not the
   same statement. */
/* Laid out rather than flowed, so the label is what gives way when the column is narrow.
   The line ellipsises, and bare text between the arrows made the TRAILING one the first
   thing cut -- the one part of the line that is a control. */
.galtx.gscope{color:var(--dim); margin-bottom:1px; display:flex; align-items:center; gap:2px;}
/* Natural width, shrinkable: the pair stays against the figure it moves rather than being
   pushed to the edges of a column that is usually wider than the line needs. */
.gscopetx{flex:0 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis;}
/* The arrows that page that list, drawn INSIDE the scope line because the scope is exactly
   what they move: which slice of the ranking is on screen.
   They are the only thing on this tile that can be pressed, so they are READ in the reading
   colour: 11.7:1 on the card, against 3.5:1 for the line they sit in. Dimmer than the label
   was measured and wrong -- at 2:1 for the live one and 1.2:1 for the dead one, the control
   was the palest thing on a tile full of text.
   Within a list that pages, the one leading nowhere stays drawn and recessed: an arrow that
   disappears at an end removes the very cue that the list ended, at the moment the reader is
   looking for it. A list of a single screen draws neither -- see geo.js. */
.galtx .gpage{flex:0 0 auto; font-style:normal; font-size:12px; line-height:1; padding:0 2px;
  color:var(--ink); cursor:pointer; user-select:none;}
.galtx .gpage:hover{color:var(--teal);}
.galtx .gpage.off{color:var(--dim2); cursor:default;}
.galtx .gpage.off:hover{color:var(--dim2);}
/* Hover target, wider than the mark and invisible. Aiming at a three-pixel dot is not a
   reading gesture. */
.ghit{fill:transparent; cursor:help;}
/* The hint appears on hover with NO delay and no transition, positioned on the mark itself
   rather than on the pointer, so it does not jitter under the hand. */
/* It wraps rather than running past the card's edge, which clips: the width is capped on the
   card by the code that places it, and a cut-off name is the one thing a hint must not have. */
.ghint{position:absolute; left:0; top:0; transform:translate(-50%,-160%); z-index:3;
  width:max-content;   /* or it would fold at the width of the narrow column it is anchored in */
  pointer-events:none; font-family:var(--mono); font-size:10px; text-align:center;
  color:var(--ink); background:#0b1218; border:1px solid var(--edge); border-radius:2px;
  padding:2px 6px; box-shadow:0 2px 8px rgba(0,0,0,.5);}
.ghint[hidden]{display:none;}
/* No transition on the marks, deliberately. A mark gliding towards its new position spends
   half a second showing a bearing that is NO LONGER TRUE -- a stale reading, not an
   animation. The tuner jumped; the target jumps with it. */
.gdist{font-family:var(--mono); font-size:12px; color:var(--dim); text-align:center; white-space:nowrap; line-height:1;}
.gdist #gBrg, .gdist #gDist{color:var(--ink);}            /* bearing and distance under the target */
.gtxt{min-width:0; line-height:1.3; overflow:hidden;}   /* fixed column width from the grid */
.gname{display:flex; align-items:center; gap:6px; min-width:0;}
/* The site is the heading of this tile. The flag sits with it, since it qualifies the place
   and not the programme. */
.gname #gSite{font:600 15px var(--sans); color:#eaf2f7; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0;}
.gname #gFlag{font-style:normal; font-size:13px; flex:0 0 auto;}
/* The country, in the list under the site. */
.gcc{font-style:normal; flex:0 0 auto;}
/* A HUE PER COUNTRY, and it is not decoration. Most systems carry no flag glyph and fall
   back to the two letters of the code, at the smallest size on the page: six of them stacked
   in the list are six grey letter pairs, and the eye separates none of them. A colour sorts
   them before they are read. Where the flag does render, the emoji is an image and ignores
   this — nothing is lost either way.
   The hues come from the flags, because a colour that has to be learnt is worth less than
   one already known. Only countries with an unmistakable colour identity are listed; the
   rest keep the neutral, which says "no claim" rather than inventing a meaning. Distant
   countries may share a family: what has to be separable is the handful that can appear on
   ONE channel, not the whole continent at once. */
.geo [data-cc="FR"]{color:#6f9dff;}   /* bleu */
.geo [data-cc="GB"]{color:#9b8ef2;}   /* Union blue */
.geo [data-cc="IE"]{color:#86cf55;}   /* green */
.geo [data-cc="NL"]{color:#ff9440;}   /* oranje */
.geo [data-cc="BE"]{color:#c9a02e;}   /* the yellow of the tricolour */
.geo [data-cc="LU"]{color:#4fc6e8;}   /* light blue */
.geo [data-cc="DE"]{color:#ebc54a;}   /* gold */
.geo [data-cc="CH"]{color:#f2695f;}   /* red */
.geo [data-cc="AT"]{color:#f0919c;}   /* red-white-red */
.geo [data-cc="IT"]{color:#3fc990;}   /* verde */
.geo [data-cc="ES"]{color:#f59a3c;}   /* rojigualda */
.geo [data-cc="AD"]{color:#d9772e;}   /* the red of the Pyrenean tricolour */
.geo [data-cc="PT"]{color:#35c2a6;}   /* verde */
.geo [data-cc="MC"]{color:#f0637a;}   /* red */
/* The programme keeps a line, quieter than the site: the logo already says it, and this is
   the fallback for a station whose logo is missing or fails to load. */
.gline #gProg{color:var(--dim);}
.gline,.gmeta{font-family:var(--mono); font-size:11px; color:var(--dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.gline b,.gmeta b{color:var(--ink); font-weight:600;}
/* Ambiguity marker: shown only when the location is uncertain, and nothing when it is not.
   Instrument spirit: only the exception is signalled. */
.gq{flex:0 0 auto; font-style:normal; font-size:12px; line-height:1; cursor:help;
  color:var(--amber);}
.gq[hidden]{display:none;}
/* The other plausible sites, under the resolved one. Recessed a step further than the
   metadata line: they are what the identity COULD be, not what the tile states. One per
   line, since a distance and a power run together are unreadable in a row. */
.galt{margin-top:3px; font-family:var(--mono); font-size:10px; color:var(--dim2);
      display:flex; flex-direction:column; gap:1px; min-width:0;}
.galt[hidden]{display:none;}
.galtx{white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
/* Plausibility relative to the resolved site, never an absolute figure: the score has no
   unit and means nothing outside the candidates of one frequency. */
.galtx i{font-style:normal; color:var(--dim); margin-left:6px;}
/* The programme, when a line carries one. It only does where every line is a DIFFERENT
   broadcaster, and there it is the information the line exists for, so it leads and it is
   the one thing here written in the reading colour. */
.galtx b{color:var(--ink); font-weight:600;}
/* The bearing rides with the name because it is the one figure that decides a gesture --
   where to point the antenna. Everything else about the site is one hover away: six lines
   packed to the ellipsis are six lines nobody reads. */
.galtx em{font-style:normal; color:var(--dim); margin-left:4px;}
.galtx[title]{cursor:help;}
/* Nothing identified: the logo slot is empty, so the programme has nowhere else to appear.
   It stops being the quiet echo of a logo and becomes the name the tile proposes. */
.geo.possible .gline #gProg{color:#c8d6e0;}
/* The continuous-trace meter and its variants live in base.css: more than one page carries
   one, and two copies of an instrument's look would drift apart. */
/* Header measurement block: label, bar, value, stackable. Shared idiom across the panel's
   widgets, which is what makes them look like instruments of the same family. Pushed to the
   right, so a header keeps controls on the left and measurement on the right. */
.wcard .fq{margin-left:auto; display:flex; flex-direction:column; gap:2px;}
.wcard .fqr{display:flex; align-items:center; gap:6px;
                 font-family:var(--mono); font-size:11px; color:var(--ink);}
/* Fixed-width, right-aligned label column, so both bars start at the same abscissa;
   otherwise labels of different lengths offset them and the block loses its composure. */
.wcard .fqr b{min-width:16px; text-align:right; color:var(--dim);
                   font-weight:400; font-size:9px; text-transform:uppercase; letter-spacing:.1em;}
.wcard .fqv{min-width:30px; text-align:right;}   /* fixed pitch, so the value does not shake the bar */
/* A fine bar, graduated as a watermark: the graduation is what distinguishes an instrument
   from a progress bar. The peak marker comes from the shared track. */
.track.mp{width:70px; height:5px; flex:0 0 auto;
          background-image:repeating-linear-gradient(90deg,#243442 0 1px,transparent 1px 25%);
          background-color:#0b1116;}
.track.mp .clip,.track.mp .grad,.track.mp .peak{display:block;}
.track.mp .grad{background:linear-gradient(90deg,var(--teal) 0%,var(--amber) 55%,var(--red) 100%);}
.track.mp .peak{width:2px; background:var(--amber); box-shadow:0 0 5px var(--amber);}
.meter.w-filter .mv{font-size:11px;}
/* Filter header: the tuner toggles plus the cyclic bandwidth button. */
.meter.w-filter .mh{align-items:center; flex-wrap:wrap;}   /* buttons centre rather than sit on a baseline */
.meter.w-filter .ftog{display:flex; gap:5px;}
.meter.w-filter .ftog .btn.tog{padding:3px 8px; font-size:10px; letter-spacing:.08em;
      touch-action:none;}                          /* compact toggles; touch behaviour left to the long press */
/* When the equaliser follows the measurement, the button keeps the real state and the small
   marker says only who decides. Full size, not a superscript: a marker that must be guessed
   is useless. Always in the DOM and merely transparent when the automation is off, so the
   button width never changes and the header never jumps. */
.meter.w-filter .ftog .btn.tog i{font-style:normal; font-size:inherit; letter-spacing:0;
      margin-left:5px; color:var(--teal); opacity:0;}
.meter.w-filter .ftog .btn.tog.auto i{opacity:.95;}
/* The AGC is a setting with four values rather than a state, so its marker carries the
   value and is always readable -- there is no "on" for the button itself to show. */
.meter.w-filter .ftog .btn.tog.agc i{opacity:.95; color:#eaf2f7;}
.meter.w-filter .bwbtn{font-family:var(--mono); font-size:11px; letter-spacing:.06em; color:#eaf2f7;
      background:#12202a; border:1px solid #22343e; border-radius:2px; padding:3px 9px; margin-left:2px;
      min-width:52px; text-align:center; cursor:pointer; user-select:none; touch-action:none;}
.meter.w-filter .bwbtn:hover{border-color:var(--teal);}
.meter.w-filter .bwbtn:active{transform:translateY(1px);}
.meter.w-filter .bwbtn.auto{color:var(--teal); border-color:#1f5048; background:#0d1f1c; font-weight:700;}
.meter.w-filter canvas{display:block; width:100%; height:46px; border-radius:2px; background:#0a0f14; border:1px solid #152029;}

/* RF impairments are drawn inside the filter itself; the header keeps only the value. */
.cn{display:flex; flex-direction:column; justify-content:flex-end; text-align:right; padding-bottom:1px;}
.cn .ik{font-size:9px; letter-spacing:.1em; text-transform:uppercase; color:var(--dim);}
.cn .cnv{font-family:var(--mono); font-size:16px; color:var(--ink);}
.cn .cnv small{font-size:9px; color:var(--dim);}

/* Expert RDS widget: the health and composition of the stream, not its content.
   Column layout: each row carries its label on the left at a fixed width, with its content
   on the right. No title lines, so five sections give back five lines to the only list that
   can genuinely overflow. The column of aligned labels also gives the composure of an
   instrument sheet. */
.meter.w-rds{font-family:var(--mono);}
/* Stream lost: the whole instrument fades. Everything on this card was read from a subcarrier
   that is no longer arriving, so dimming one field of it would be a half-truth -- the block
   rate would stay bright next to a group profile nobody is receiving any more. Kept rather than
   blanked, the same convention as the identity: the last known state, without the claim that it
   is current. A transition, because RDS comes and goes at the edge of reception and a card
   flicking between two opacities is unreadable. */
.meter.w-rds{transition:opacity .3s ease;}
.meter.w-rds.stale{opacity:.42;}
/* Header on the filter's template: title on the left, measurement on the right. The right
   padding leaves room for the navigation chevrons floating in the corner. */
.meter.w-rds .mh{align-items:center; gap:9px; margin-bottom:6px; padding-right:44px;}
.meter.w-rds .rcap{flex:0 0 auto; font-size:10px; letter-spacing:.14em; text-transform:uppercase;
                   color:var(--dim);}
/* The stacked bar matches the header bars of the filter -- same height, same background --
   so both widgets align their measurements exactly. */
.meter.w-rds .rstack{flex:1; min-width:70px; display:flex; height:13px; border-radius:2px;
     overflow:hidden; background:#0b1116; border:1px solid #182430;
     box-shadow:inset 0 1px 2px rgba(0,0,0,.6);}
.meter.w-rds .rstack span{height:100%; width:0; transition:width .25s ease;}
.meter.w-rds .rstack .e0{background:var(--green);}
.meter.w-rds .rstack .e1{background:var(--amber);}
.meter.w-rds .rstack .e2{background:#c98a2a;}
.meter.w-rds .rstack .e3{background:var(--red);}
/* A row is a fixed label column plus content. The label aligns to the top, so content that
   wraps over several lines does not push it to the middle. */
.meter.w-rds .rr{display:flex; gap:8px; margin-bottom:5px; min-width:0;}
.meter.w-rds .rr:last-child{margin-bottom:0;}
.meter.w-rds .rk{flex:0 0 46px; padding-top:2px; font-size:9px; letter-spacing:.1em;
                 text-transform:uppercase; color:var(--dim2); line-height:13px;}
.meter.w-rds .rk b{color:var(--dim); font-weight:400;}   /* count attached to the label */
/* Compact chips. For groups, opacity carries the share, so the profile reads by contrast. */
.meter.w-rds .rgrp{display:flex; flex-wrap:wrap; gap:3px; align-content:flex-start;
                   min-width:0; min-height:15px;}
.meter.w-rds .gchip{font-size:10px; line-height:15px; color:var(--teal); background:#0d1f1c;
     border:1px solid #1f5048; border-radius:2px; padding:0 4px; white-space:nowrap;}
.meter.w-rds .gchip b{color:var(--dim); font-weight:400; margin-left:3px; font-size:9px;}
.meter.w-rds .gnone{color:var(--dim2); font-size:11px; line-height:15px;}
/* The only elastic row: it takes all the free height and scrolls beyond it. */
.meter.w-rds .rgrow{flex:1 1 auto; min-height:0;}
.meter.w-rds .rgrow .rgrp{flex:1; min-height:0; overflow-y:auto;}
/* Frequency chips take a neutral tint, distinct from the group chips, which speak about the
   protocol. Tabular figures keep the grid regular whatever the value. */
.meter.w-rds .raf .gchip{color:var(--ink); background:#101820; border-color:#22343e;
                         font-variant-numeric:tabular-nums;}
/* A station may include its own frequency in its list: it is shown, not hidden. */
.meter.w-rds .raf .gchip.self{color:var(--teal); border-color:#1f5048; background:#0d1f1c;}
/* Application chips carry a distinct tint from the raw group chips. */
.meter.w-rds .gchip.oda{color:var(--amber); background:#1a1508; border-color:#3d3115;}
.meter.w-rds .gchip.oda b{color:var(--dim);}
/* Announced networks: the identity code, always present, plus a name that is often partial.
   They wrap, so two short entries fit side by side. */
.meter.w-rds .reon{display:flex; flex-wrap:wrap; gap:2px 10px; font-size:11px;
                   line-height:15px; min-height:15px; min-width:0;}
.meter.w-rds .eline{display:inline-flex; align-items:center; gap:5px; white-space:nowrap;}
.meter.w-rds .eline b{color:var(--teal); font-weight:400; font-variant-numeric:tabular-nums;}
.meter.w-rds .eps{color:var(--ink);}
/* The announced traffic flags and the declared flags share one small template. An active
   traffic announcement is the historical purpose of the feature, and the only thing in this
   widget that deserves a strong colour. */
.meter.w-rds .et{font-style:normal; font-size:8px; letter-spacing:.08em; color:var(--dim2);
                 border:1px solid #1f2a33; border-radius:2px; padding:0 3px; line-height:12px;}
.meter.w-rds .et.on{color:#241c02; background:var(--amber); border-color:var(--amber);}
/* Declared states in the header, between the title and the measurement. They are qualifiers
   of the station, so they border the measurement instead of taking a line of their own, and
   each disappears when not received: an instrument header is not a form and reserves no
   empty box for what the station does not emit. If space runs short, these give way, never
   the measurement. */
.meter.w-rds .rst{display:flex; align-items:center; gap:7px; min-width:0; overflow:hidden;
                  font-size:10px; color:var(--dim); white-space:nowrap;}
.meter.w-rds .rclk:not(:empty){color:var(--ink); font-variant-numeric:tabular-nums;}
.meter.w-rds .rms:not(:empty){color:var(--dim);}
/* The country code is a raw hex pair, so it is set in the monospace face the rest of the
   measured values use — it reads as a code, not as a word. */
.meter.w-rds .recc:not(:empty){color:var(--dim); font-family:var(--mono); letter-spacing:.04em;}
.meter.w-rds .di{display:inline-flex; gap:3px; flex:0 0 auto;}
.meter.w-rds .di .et.on{color:#06231c; background:var(--teal); border-color:var(--teal);}

/* Widget navigation: two chevrons in the top corner of each slot. Quiet at rest, since this
   is a layout command and not an instrument, and clear when the card is hovered. They are
   not in the widgets' markup: a widget does not carry the controls that replace it. */
.wcard{position:relative;}
.wnav{position:absolute; top:4px; right:6px; z-index:3; display:flex; gap:1px;
      opacity:0; transition:opacity .15s ease;}
.wcard:hover .wnav, .wnav:focus-within{opacity:1;}
.wnav i{font-style:normal; font-size:14px; line-height:1; color:var(--dim2); cursor:pointer;
        padding:1px 5px 3px; border-radius:3px; user-select:none;}
.wnav i:hover{color:var(--ink); background:#172530;}
.wnav i:active{transform:translateY(1px);}
/* Touch, where there is no hover: always visible but faded. */
@media (hover:none){ .wnav{opacity:.45;} }

/* Announced networks, one line each. */
.meter.w-rds .reon{display:flex; flex-wrap:wrap; gap:3px 10px; font-size:11px; min-height:15px;}
.meter.w-rds .eline{display:inline-flex; align-items:center; gap:5px; white-space:nowrap;}
.meter.w-rds .eline b{color:var(--teal); font-weight:400; font-variant-numeric:tabular-nums;}
.meter.w-rds .eps{color:var(--ink);}
/* Traffic flags of an announced network: an active one means traffic information is running
   over there right now. */
.meter.w-rds .et{font-style:normal; font-size:8px; letter-spacing:.08em; color:var(--dim2);
                 border:1px solid #1f2a33; border-radius:2px; padding:0 3px; line-height:12px;}
.meter.w-rds .et.on{color:#241c02; background:var(--amber); border-color:var(--amber);}

/* RDS quality is the same instrument as the signal meter, beside it, because RDS is the
   other received channel and its health reads like a level. A shorter bar, since a
   percentage needs no graduation, and above all a solid bar whose hue is the measurement.
   The colour is set from the script, so no background is declared here -- it would be
   overwritten -- and there is no threshold: the ramp is continuous. */
.lhead .rdsq{flex:0 0 auto;}   /* attached to its own flags, in the same block */
.lhead .rdsq .nbody{width:120px;}
/* The peak marker stays white, like the signal meter: the track colour already states the
   quality, and a tinted marker would compete with it. A marker marks a position; it does
   not judge. */
.track.rq .grad{transition:background .25s linear;}   /* the hue slides, so no colour jumps between measurements */
.lhead .rdsq .lvlbig{margin-left:8px;}

/* Application chips. */
.meter.w-rds .gchip.oda{color:var(--amber); background:#1a1508; border-color:#3d3115;}
.meter.w-rds .gchip.oda b{color:var(--dim);}
/* Flags the station declares about itself, at the end of the rare-field row, on the same
   template as the network flags. A flag never received is not rendered at all, which is not
   the same as being declared false. */
.meter.w-rds .di{margin-left:auto; display:inline-flex; gap:3px;}
.meter.w-rds .di .et.on{color:#06231c; background:var(--teal); border-color:var(--teal);}

/* Tagged text: the class name small, the value plain -- the content reads first and the
   label is only a qualifier. Wraps like the chips. */
.meter.w-rds .rtp{display:flex; flex-wrap:wrap; gap:3px 8px; min-width:0; min-height:15px;}
.meter.w-rds .tag{display:inline-flex; align-items:baseline; gap:4px;
                  font-size:11px; line-height:15px; color:var(--ink); min-width:0;}
.meter.w-rds .tag i{font-style:normal; font-size:8px; letter-spacing:.08em; text-transform:uppercase;
                    color:var(--teal); flex:0 0 auto;}
/* A finished item keeps its tags but they are stale, so they are dimmed rather than erased.
   Same convention as an identity kept when RDS is lost. */
.meter.w-rds .tag.stale{opacity:.5;}

/* A confirmed alternative frequency, meaning the same reliable identity was found on it,
   takes the amber accent. A merely announced one stays neutral: that is the difference
   between "the broadcaster claims it" and "we verified it". The frequency being listened to
   wins, being confirmed by construction. */
.meter.w-rds .raf .gchip.known{color:var(--amber); border-color:#3d3115; background:#151107;}
/* Clickable: a click tunes. The cursor and the hover say so, without a button or a label. */
.meter.w-rds .afm{font-style:normal; color:var(--dim); font-size:9px; margin-left:3px;}
/* Markers on a frequency chip -- the regional R, the multipath M -- share one treatment.
   Both say the same thing: read this entry with a reservation. A second style would only
   invent a hierarchy between two reservations that do not have one. */
.meter.w-rds .raf .gchip i{font-style:normal; font-size:8px; vertical-align:top;
  margin-left:1px; opacity:.8;}
/* The winner of the survey, outlined rather than filled: it is a RESULT, not a state of
   the receiver, which has not moved there unless it was asked to. It is also the ONLY
   thing the survey draws -- everything it measured is read on hover, one entry at a time,
   because forty chips carrying a figure each stop being a list. */
.meter.w-rds .raf .gchip.best{border-color:var(--teal); color:#eaf2f7;}
/* The row's own two controls, stacked in its label column so they take nothing from the
   chips. Wrapping is allowed: the second one names its criterion, and a control that
   cannot say what it decides on is a control nobody presses twice. */
.meter.w-rds .afbtn{display:flex; flex-direction:column; gap:2px; margin-top:3px;}
/* The label has to hold ONE line, or the destination under it becomes a third. In Plex
   Mono every glyph is 0.6 em, so BEST C/N is 8 x 4.8 = 38.4 px at 8 px, and the column
   leaves 42 px once the border and padding are taken off. At .05 em the tracking added
   3.2 px and left 0.4 px of margin -- fitting, but on an amount no layout should depend
   on. At .02 em it is 39.7 px. Wrapping stays ALLOWED so a fallback font folds the label
   instead of overflowing the column. */
.meter.w-rds .afbtn button{font-family:inherit; font-size:8px; letter-spacing:.02em;
  line-height:10px; text-transform:uppercase; color:var(--dim); background:#101820;
  border:1px solid #22343e; border-radius:2px; padding:2px 1px; cursor:pointer;
  white-space:normal;}
.meter.w-rds .afbtn button:hover{border-color:var(--teal); color:#eaf2f7;}
/* The destination, on its own line under the label: the button is 8 px uppercase, the
   frequency is a VALUE, so it drops the letter-spacing and takes tabular figures.
   `color:inherit` is the load-bearing one and has to be written: this button sits inside
   the label column, where `.rk b` dresses the reading counts in dim -- so the number was
   painted grey while the control above it went green, which is the one thing this line is
   for. Following the button makes the verdict and the frequency it was read on turn
   together, on hover as on the green. */
.meter.w-rds .afbtn button b{display:block; color:inherit; font-weight:400; font-size:9px;
  line-height:11px; letter-spacing:0; text-transform:none;
  font-variant-numeric:tabular-nums;}
/* The survey's answer is "stay": the receiver is already on the best of it. */
.meter.w-rds .afbtn button.onbest{border-color:var(--green); color:var(--green);}
.meter.w-rds .afbtn button:active{transform:translateY(1px);}
/* How old the survey is. It sits under the controls that produced it, because that is the
   only figure that says whether its numbers still describe the band. */
.meter.w-rds .afage{display:block; font-style:normal; font-size:8px; color:var(--dim2);
  margin-top:2px; font-variant-numeric:tabular-nums;}
/* A long or medium wave entry: announced, and out of reach of this receiver. Dimmed and
   left with the default cursor, since it is the one chip in the row a click cannot act on
   -- a pointer over it would promise a move that never happens. */
.meter.w-rds .raf .gchip.lfmf{color:var(--dim); background:#141414; border-color:#2b2b2b;
  cursor:default;}
.meter.w-rds .raf .gchip.lfmf:hover{border-color:#2b2b2b; color:var(--dim);}
.meter.w-rds .raf .gchip{cursor:pointer;}
.meter.w-rds .raf .gchip:hover{border-color:var(--teal); color:#eaf2f7;}
.meter.w-rds .raf .gchip:active{transform:translateY(1px);}
