/* OBEINSON FREQUENCY X — the station chart on the app landing.
 *
 * Two ranked columns in the shape of an app-store top ten: rank, artwork,
 * name, a quieter line under it, and a movement marker on the right.
 *
 * Side by side where there is room, stacked on a phone. The reference artwork
 * is a wide graphic; two columns of ten rows at 393px would give each name
 * about half a word, so below 760px they become one column after the other,
 * which keeps every row readable at the size it is actually read.
 */

/* Three across at every width, as the reference does.
 *
 * Three columns of list rows do NOT fit a phone if the row keeps a rank
 * column, a marker column and a 30px tile: measured at 393px, that left the
 * name box 10px wide and clipped every single name to nothing. So the narrow
 * layout drops to what the reference actually shows — a small tile and a
 * label — and the rank rides on the tile instead of taking a column of its
 * own. The marker is the one thing genuinely lost, and it comes back as soon
 * as there is room for it. */
.obnChart {
  margin: 12px 8px 16px;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  align-items: start;      /* each plate is only as tall as its own list */
  gap: 8px;
}
@media (min-width: 560px) { .obnChart { margin: 14px 12px 18px; gap: 12px; } }
@media (min-width: 980px) { .obnChart { gap: 16px; } }

/* The panel reads as a HUD plate: a cut corner instead of a fourth radius, a
   hairline grid behind the rows, and a lit edge in the column's own colour.
 *
 * NOTHING HERE ANIMATES ON ITS OWN. This app's idle CPU was 97% and most of
 * that was style recalculation; a pulsing glow or a travelling gradient is a
 * compositor job every frame, forever, on a page people leave open to listen.
 * The only movement is on :hover, which costs nothing until a pointer is on
 * the row — and a phone, where this mostly runs, has no hover at all.
 *
 * The grid and the cut corner are a static background-image and a static
 * clip-path: both are painted once and cost nothing after that. */
.obnChartCol {
  --accent: #35c8ff;
  --glow: rgba(53, 200, 255, .55);
  position: relative;
  min-width: 0;
  padding: 9px 7px 6px;
  background:
    /* the hairline grid, two one-pixel lines repeated */
    repeating-linear-gradient(0deg, rgba(130, 170, 210, .05) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(90deg, rgba(130, 170, 210, .05) 0 1px, transparent 1px 34px),
    linear-gradient(165deg, rgba(27, 41, 58, .96), rgba(11, 17, 26, .97));
  border: 1px solid rgba(130, 170, 215, .20);
  border-top-color: var(--accent);
  border-radius: 14px 0 14px 14px;
  /* the cut corner, top right */
  clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, .35),
    0 10px 30px rgba(0, 0, 0, .40),
    inset 0 1px 0 rgba(255, 255, 255, .05);
}
/* The lit edge along the top, fading out to the cut.
   This is a real element rather than a ::before because it breathes while a
   station plays, and Chromium cannot composite an animation on a pseudo
   element — it repaints it on the main thread every frame. Measured on a
   4x-throttled phone: as pseudo elements the pulse cost ~10 points of main
   thread (53.7% -> 63.7%) and was the entire cost of the motion; the sweep
   beside it, a transform on a real element, was free. */
.obnChartEdge {
  position: absolute;
  inset: 0 0 auto;
  height: 2px;
  pointer-events: none;
  background: linear-gradient(90deg, var(--accent), var(--accent) 22%, transparent 78%);
  box-shadow: 0 0 10px var(--glow);
}
/* a bracket in the bottom-left corner, the way a readout frames itself */
.obnChartCol::after {
  content: '';
  position: absolute;
  left: 7px; bottom: 7px;
  width: 13px; height: 13px;
  border-left: 1px solid var(--accent);
  border-bottom: 1px solid var(--accent);
  opacity: .5;
}
.obnChartCol.popular { --accent: #ff5fc8; --glow: rgba(255, 95, 200, .55); }

.obnChartHead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 2px 7px;
  margin: 0 1px 8px;
  padding-bottom: 7px;
  border-bottom: 1px solid rgba(130, 170, 215, .16);
}
/* Tracked-out mono caps: the label of an instrument rather than a headline.
   text-transform does this, so the markup still says "Most played" and a
   screen reader still reads it as a sentence. */
.obnChartHead h3 {
  margin: 0;
  font: 700 9px/1.25 var(--mono, ui-monospace, monospace);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: var(--accent);
  -webkit-text-fill-color: var(--accent);
  text-shadow: 0 0 14px var(--glow);
}
.obnChartHead span {
  display: none;
  font: 500 10px/1.3 var(--mono, monospace);
  text-transform: uppercase;
  letter-spacing: .09em;
  color: #7e93b0;
  -webkit-text-fill-color: #7e93b0;
}

/* A row is a slot on a readout: a dim rule down its left edge that lights up
   and thickens when it is the one under the pointer. The transition runs only
   while that is happening — there is no resting animation. */
.obnChartRow {
  position: relative;
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  padding: 4px 3px 4px 7px;
  border: 0;
  background: none;
  width: 100%;
  text-align: left;
  cursor: pointer;
  border-radius: 0 9px 9px 0;
  -webkit-tap-highlight-color: transparent;
  transition: background .18s ease;
}
.obnChartRow::before {
  content: '';
  position: absolute;
  left: 0; top: 6px; bottom: 6px;
  width: 2px;
  border-radius: 2px;
  background: rgba(130, 170, 215, .22);
  transition: background .18s ease, box-shadow .18s ease;
}
.obnChartRow:hover, .obnChartRow:focus-visible {
  background: linear-gradient(90deg, rgba(130, 170, 215, .13), transparent 72%);
}
.obnChartRow:hover::before, .obnChartRow:focus-visible::before {
  background: var(--accent);
  box-shadow: 0 0 9px var(--glow);
}
.obnChartRow:active { background: linear-gradient(90deg, rgba(0, 225, 255, .18), transparent 72%); }

/* The top three are coloured the way a chart colours them. The number itself
   still says the position, so the colour repeats information rather than
   carrying it — nothing is lost without colour vision. */
.obnChartRank {
  position: absolute;
  left: 2px; top: 1px;
  z-index: 1;
  font: 700 8.5px/1 var(--mono, ui-monospace, monospace);
  letter-spacing: 0;
  color: rgba(255, 255, 255, .72);
  -webkit-text-fill-color: rgba(255, 255, 255, .72);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .75);
  text-align: left;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
/* the rank badge rides on the tile, so the row itself is positioned */
.obnChartRow { position: relative; }
.obnChartMove { display: none; }
.obnChartRank.r1 { color: #ffd36a; -webkit-text-fill-color: #ffd36a; text-shadow: 0 0 12px rgba(255, 211, 106, .55); }
.obnChartRank.r2 { color: #cfdae8; -webkit-text-fill-color: #cfdae8; text-shadow: 0 0 10px rgba(207, 218, 232, .40); }
.obnChartRank.r3 { color: #e6a269; -webkit-text-fill-color: #e6a269; text-shadow: 0 0 10px rgba(230, 162, 105, .40); }

/* Artwork, or initials when a station has no logo — never an empty square.
 *
 * Eight tile colours instead of one, picked by a hash of the station name, so
 * a column reads as a chart of distinct stations rather than one repeated
 * swatch. The station keeps its colour between visits because the hash is of
 * the name, not the position.
 *
 * The eight were chosen against measured contrast, not by eye: white on each
 * gradient's MIDPOINT — which is what the glyph actually sits on — ranges from
 * 5.41:1 (amber) to 8.92:1 (indigo), so the 16px initials clear the 4.5:1 that
 * normal-size text needs. Brighter versions of the same hues looked better and
 * failed it, amber and green worst.
 */
.obnChartArt {
  position: relative;
  width: 28px; height: 28px;
  border-radius: 8px 2px 8px 2px;        /* two cut corners, same motif as the panel */
  overflow: hidden;
  display: grid;
  place-items: center;
  background: linear-gradient(145deg, #2f6bdb, #1b3f9e);
  color: #fff;
  -webkit-text-fill-color: #fff;
  font: 800 10px/1 var(--mono, ui-monospace, monospace);
  letter-spacing: .02em;
  flex: none;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, .22),   /* a lit rim around the tile */
    inset 0 -8px 14px rgba(0, 0, 0, .28),
    0 2px 8px rgba(0, 0, 0, .45);
}
.obnChartArt.c0 { background: linear-gradient(145deg, #2f6bdb, #1b3f9e); }
.obnChartArt.c1 { background: linear-gradient(145deg, #7148e8, #4a22ab); }
.obnChartArt.c2 { background: linear-gradient(145deg, #c7308a, #8d1a62); }
.obnChartArt.c3 { background: linear-gradient(145deg, #d8402f, #9c1f1c); }
.obnChartArt.c4 { background: linear-gradient(145deg, #b86a08, #8a4406); }
.obnChartArt.c5 { background: linear-gradient(145deg, #128a5c, #0a5c3c); }
.obnChartArt.c6 { background: linear-gradient(145deg, #0e8a92, #076068); }
.obnChartArt.c7 { background: linear-gradient(145deg, #3348d6, #1e2a96); }
.obnChartArt img { width: 100%; height: 100%; object-fit: cover; display: block; }

.obnChartText { min-width: 0; }
.obnChartName {
  display: block;
  font: 700 11.5px/1.2 var(--ui, system-ui, sans-serif);
  color: #f2f7ff;
  -webkit-text-fill-color: #f2f7ff;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The second line is dropped below 980px, where the three columns are side
   by side and a name already has only so much room. It comes back on a wide
   screen, where there is space for it. The markup always carries it, so the
   station's country and genre are still there for a screen reader. */
.obnChartSub {
  display: none;
  margin-top: 2px;
  font: 500 10px/1.3 var(--mono, monospace);
  text-transform: uppercase;
  letter-spacing: .06em;
  color: #7e93b0;
  -webkit-text-fill-color: #7e93b0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* From 560px there is room for the rank and the marker to have columns of
   their own again, and the tile can grow back. */
@media (min-width: 560px) {
  .obnChartHead span { display: inline; }
  .obnChartHead h3 { font-size: 11px; letter-spacing: .12em; }
  .obnChartRow { grid-template-columns: 20px 30px minmax(0, 1fr) 16px; gap: 8px; padding: 4px 3px 4px 8px; }
  .obnChartArt { width: 30px; height: 30px; font-size: 11px; }
  .obnChartName { font-size: 12px; }
  .obnChartMove { display: block; }
  .obnChartRank {
    position: static;
    font-size: 11px;
    color: #74889f; -webkit-text-fill-color: #74889f;
    text-shadow: none; text-align: center;
  }
  .obnChartRank.r1 { color: #ffd36a; -webkit-text-fill-color: #ffd36a; }
  .obnChartRank.r2 { color: #cfdae8; -webkit-text-fill-color: #cfdae8; }
  .obnChartRank.r3 { color: #e6a269; -webkit-text-fill-color: #e6a269; }
}
@media (min-width: 980px) {
  .obnChartSub { display: block; }
  .obnChartRow { grid-template-columns: 22px 36px minmax(0, 1fr) 18px; padding: 6px 3px 6px 8px; }
  .obnChartArt { width: 36px; height: 36px; font-size: 13px; }
  .obnChartName { font-size: 13.5px; }
}

/* Movement since the last visit. Colour alone never carries it — each marker
   is a different glyph, so it still reads without colour vision. */
.obnChartMove { font: 700 13px/1 var(--ui, system-ui, sans-serif); text-align: center; }
.obnChartMove.up   { color: #45e3a0; -webkit-text-fill-color: #45e3a0; }
.obnChartMove.down { color: #f2665c; -webkit-text-fill-color: #f2665c; }
.obnChartMove.new  { color: #ffd400; -webkit-text-fill-color: #ffd400; }
.obnChartMove.same { color: #5b6c82; -webkit-text-fill-color: #5b6c82; }

.obnChartEmpty {
  margin: 2px 4px 10px;
  font: 500 12px/1.5 var(--mono, monospace);
  color: #8ba0bd;
  -webkit-text-fill-color: #8ba0bd;
}

:root[data-theme="light"] .obnChartCol {
  background: linear-gradient(180deg, #ffffff, #f4f8fb);
  border-color: #d3dde8;
}
/* Light theme: the same plate, lit from a white room. The glow tokens drop to
   almost nothing, because a coloured halo on white is grime, not light. */
:root[data-theme="light"] .obnChartCol {
  --accent: #0a74a8;
  --glow: rgba(10, 116, 168, .20);
  background:
    repeating-linear-gradient(0deg, rgba(20, 60, 95, .045) 0 1px, transparent 1px 34px),
    repeating-linear-gradient(90deg, rgba(20, 60, 95, .045) 0 1px, transparent 1px 34px),
    linear-gradient(165deg, #ffffff, #eef4fa);
  border-color: #d3dde8;
  border-top-color: var(--accent);
  box-shadow: 0 6px 20px rgba(20, 50, 80, .10), inset 0 1px 0 rgba(255, 255, 255, .8);
}
:root[data-theme="light"] .obnChartCol.popular { --accent: #b02a7d; --glow: rgba(176, 42, 125, .20); }
:root[data-theme="light"] .obnChartHead h3 { text-shadow: none; }
:root[data-theme="light"] .obnChartRank.r1,
:root[data-theme="light"] .obnChartRank.r2,
:root[data-theme="light"] .obnChartRank.r3 { text-shadow: none; }
:root[data-theme="light"] .obnChartRow::before { background: rgba(20, 60, 95, .16); }
:root[data-theme="light"] .obnChartRank.r1 { color: #b8860b; -webkit-text-fill-color: #b8860b; }
:root[data-theme="light"] .obnChartRank.r2 { color: #6b7a8c; -webkit-text-fill-color: #6b7a8c; }
:root[data-theme="light"] .obnChartRank.r3 { color: #a9631f; -webkit-text-fill-color: #a9631f; }
:root[data-theme="light"] .obnChartHead span,
:root[data-theme="light"] .obnChartSub,
:root[data-theme="light"] .obnChartRank,
:root[data-theme="light"] .obnChartEmpty { color: #55687e; -webkit-text-fill-color: #55687e; }
:root[data-theme="light"] .obnChartName { color: #15212d; -webkit-text-fill-color: #15212d; }


/* ===========================================================================
   MOTION — and it runs ONLY while a station is actually playing.
   ===========================================================================

   The chart host gets `live` from station-chart.js when audio is on air, and
   every rule below is behind that class. With nothing playing the page is as
   motionless as it was before: 0 frames rendered over 10 idle seconds, which
   is the number to re-check if any of this is ever touched.

   Only `transform` and `opacity` are animated. Both are handled by the
   compositor and need no layout and no style recalculation per frame — this
   app's idle cost was almost all style recalculation, so animating a
   box-shadow or a filter here (the obvious way to make a glow breathe) would
   have put that straight back. The glow breathes by fading a second copy of
   the edge in and out instead, which is an opacity animation.

   `will-change` is deliberately NOT set. It would hand each panel its own
   compositor layer for the whole session, including the entire time nothing
   is playing, and the memory cost of that outlives the animation it helps.

   Anyone who has asked their system for less motion gets none of it: the
   whole block is inside prefers-reduced-motion: no-preference. */

/* The band's own appearance sits OUTSIDE the motion query, and only the
   movement is inside it. With the two mixed together, a reader who asks for
   reduced motion got an element with no position and no opacity rule at all —
   it was invisible only because it happens to be empty, which is luck, not
   design. Hidden by default; the motion block is the only thing that reveals
   it, so reduced motion means no band rather than a band frozen mid-sweep. */
.obnChartScan {
  position: absolute;
  inset: 0 0 auto;
  height: 46%;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(
    180deg,
    transparent,
    color-mix(in srgb, var(--accent) 12%, transparent),
    transparent
  );
}

@media (prefers-reduced-motion: no-preference) {

  .obnChart.live .obnChartScan {
    opacity: 1;
    animation: obnChartSweep 5.5s linear infinite;
  }
  @keyframes obnChartSweep {
    from { transform: translate3d(0, -100%, 0); }
    to   { transform: translate3d(0, 260%, 0); }
  }

  /* the lit top edge breathes, by fading rather than by re-blurring a shadow */
  .obnChart.live .obnChartEdge {
    animation: obnChartPulse 2.8s ease-in-out infinite;
  }
  @keyframes obnChartPulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: .45; }
  }

  /* The corner bracket does NOT pulse. It is a ::after, so its animation
     cannot be composited, and a second decorative beat is not worth putting
     a per-frame main-thread repaint back into a page people leave open. It
     stays lit instead. */
}

/* A browser without color-mix still gets a visible band rather than nothing. */
@supports not (color: color-mix(in srgb, red 10%, transparent)) {
  .obnChartScan { background: linear-gradient(180deg, transparent, rgba(255, 255, 255, .06), transparent); }
}


/* ===========================================================================
   THE BROWSE PLATE — the catalog's collections, stacked under "Most played".
   ===========================================================================

   The first grid cell holds two plates rather than one, so a short "Most
   played" does not leave a hole beside a column of ten. */

.obnChartStack {
  display: grid;
  align-content: start;
  gap: 8px;
  min-width: 0;
}

.obnBrowse { --accent: #9b7bff; --glow: rgba(155, 123, 255, .5); }

.obnBrowseGrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  padding: 0 1px 2px;
}

/* A chip is a button carrying an emoji and a name, and the name is the part
   that matters, so it truncates rather than wraps — a wrapped chip makes the
   whole grid ragged and costs more height than the plate is filling. */
.obnBrowseChip {
  min-width: 0;
  padding: 6px 7px;
  border: 1px solid rgba(130, 170, 215, .20);
  border-radius: 7px 2px 7px 2px;       /* the same cut-corner motif */
  background: linear-gradient(160deg, rgba(130, 170, 215, .11), rgba(130, 170, 215, .04));
  color: #dce7f5;
  -webkit-text-fill-color: #dce7f5;
  font: 600 10.5px/1.25 var(--ui, system-ui, sans-serif);
  text-align: left;
  cursor: pointer;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  -webkit-tap-highlight-color: transparent;
  transition: background .18s ease, border-color .18s ease;
}
.obnBrowseChip:hover, .obnBrowseChip:focus-visible {
  background: linear-gradient(160deg, rgba(155, 123, 255, .26), rgba(155, 123, 255, .10));
  border-color: var(--accent);
}
.obnBrowseChip:active { background: rgba(155, 123, 255, .34); }

@media (min-width: 560px) {
  .obnChartStack { gap: 12px; }
  .obnBrowseGrid { gap: 6px; }
  .obnBrowseChip { font-size: 11.5px; padding: 7px 9px; }
}

:root[data-theme="light"] .obnBrowse { --accent: #6b3fd4; --glow: rgba(107, 63, 212, .18); }
:root[data-theme="light"] .obnBrowseChip {
  border-color: #d3dde8;
  background: linear-gradient(160deg, #ffffff, #eef4fa);
  color: #1d2a3a;
  -webkit-text-fill-color: #1d2a3a;
}


/* ---------------------------------------------------------------------------
   The More plate — the controls taken off the phone's top bar.
   Same chip grid as Browse, its own accent so the two plates are told apart,
   and chips that span the full width when there are few of them.
   --------------------------------------------------------------------------- */
.obnMore { --accent: #ffb039; --glow: rgba(255, 176, 57, .5); }

.obnMoreChip {
  font-weight: 700;
}
.obnMoreChip:hover, .obnMoreChip:focus-visible {
  background: linear-gradient(160deg, rgba(255, 176, 57, .26), rgba(255, 176, 57, .10));
  border-color: var(--accent);
}
.obnMoreChip:active { background: rgba(255, 176, 57, .34); }

:root[data-theme="light"] .obnMore { --accent: #a7650a; --glow: rgba(167, 101, 10, .18); }
