/* ============================================================================
   AUDIO WIDGET PROTOTYPE
   Built from Figma "Audio Widgets": 2524:126999 and 2526:135968 (podcast, 350
   and 1280), 2526:142928 and 2527:144307 (live radio).

   Both widgets live on this page at once, and each one sizes itself from its
   own width with container queries rather than a manual switch. Container
   queries, not media queries, because an embeddable widget has to respond to
   the slot it is dropped into, which is not always the viewport.
   ==========================================================================*/
:root {
  --ihr-grey-100: #f6f8f9; --ihr-grey-150: #edf1f3; --ihr-grey-200: #e6eaed; --ihr-grey-250: #dadfe3;
  --ihr-grey-300: #a9afb2; --ihr-grey-450: #55565b; --ihr-grey-600: #27292d;
  --ihr-red-400: #e22c3a; --ihr-red-550: #cc032e; --ihr-red-600: #c6002b;
  --ihr-red-650: #b30027; --true-white: #fff;

  /* ---- accomplice scales, transcribed from themes/default/theme-tokens.ts.
     The token NAME is the pixel value throughout, because accomplice runs a
     10px rem base: fontSize[14] is 1.4rem, space[12] is 1.2rem. Written here in
     px so nothing depends on this page's root size. ---- */
  --fs-6: 6px;    --fs-10: 10px;  --fs-11: 11px;  --fs-12: 12px;  --fs-13: 13px;
  --fs-14: 14px;  --fs-15: 15px;  --fs-16: 16px;  --fs-17: 17px;  --fs-18: 18px;
  --fs-20: 20px;  --fs-21: 21px;  --fs-24: 24px;  --fs-32: 32px;  --fs-40: 40px;
  --lh-8: 8px;    --lh-14: 14px;  --lh-16: 16px;  --lh-18: 18px;  --lh-20: 20px;
  --lh-21: 21px;  --lh-22: 22px;  --lh-24: 24px;  --lh-26: 26px;  --lh-28: 28px;
  --lh-30: 30px;  --lh-40: 40px;  --lh-48: 48px;
  /* letterSpacing is in rem in the source and NEGATIVE for most kinds. */
  --ls-0: -0.2px; --ls-1: -0.5px; --ls-2: -0.6px; --ls-3: -2px;
  --ls-4: 0px;    --ls-5: 1px;    --ls-6: -1px;
  --sp-0: 0px;    --sp-2: 2px;    --sp-4: 4px;    --sp-6: 6px;   --sp-8: 8px;
  --sp-12: 12px;  --sp-16: 16px;  --sp-24: 24px;  --sp-28: 28px; --sp-32: 32px;
  --sp-48: 48px;
  /* brandRed is a named token in themes/default/theme-tokens.ts, sitting beside
     brandBlack, brandGray and brandWhite, and its value is the same as red600.
     Named separately here because the intent is "the brand colour", not "the
     600 step", and those two could diverge. */
  --ihr-brand-red: #c6002b;
  --ihr-gray-200: #e6eaed; --ihr-gray-300: #a9afb2; --ihr-gray-400: #717277;
  --ihr-gray-350: #96979f; --ihr-gray-500: #3f4447; --ihr-blue-400: #3ca2dd; --border-2: 2px;
  --ihr-blue-100: #9adaff; --ihr-blue-600: #0055b7;
  --rad-1: 1.25px; --rad-2: 2.5px; --rad-5: 5px; --rad-6: 6px;
  /* 99rem on accomplice's 10px base, written out so it does not follow THIS
     page's root size. Fully round either way, but the number should be theirs. */
  --rad-999: 990px;
  --elevation1: 0 1px 3px rgba(0, 0, 0, 0.15);
  --radius-widget: 6px; --shadow-01: 0 1px 3px 0 rgba(0,0,0,.12);
  /* Overlay strength over the artwork colour. Was 70%. */
  /* The drawer curves. `ease` is what accomplice gets by declaring
     `animation: slideInBottom 600ms` with no timing function, and its tail is
     gentle but its start is gentle too, so the whole thing drifts rather than
     travels. This entry curve leaves hard and decelerates the whole way in,
     which is the soft landing. A deliberate step past the component, asked for
     after comparing the two side by side. */
  --drawer-in: cubic-bezier(.32, .72, 0, 1);
  --drawer-out: cubic-bezier(.4, 0, .6, 1);
  --overlay-pct: .60;
  --overlay: rgba(0,0,0,.60);
  /* The plate the artwork cards lay their picture on, under the scrim. A theme
     colour, not something read off the artwork. Dark theme flips this. */
  --stage-plate: var(--true-white);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* The page ground, switchable on the prototype page. #e9edef is the grey these
   cards have always been judged on; white is what most publisher pages actually
   are, and a dark card reads differently against each. The attribute lives on
   <body> because that is the element being painted; every other page has no
   attribute and keeps the grey. */
body[data-bg="white"] { background: var(--true-white); }
body {
  background: #e9edef; color: var(--ihr-grey-600);
  font: 14px/1.4 'Open Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  -webkit-font-smoothing: antialiased; min-height: 100vh;
}
header {
  background: #fff; border-bottom: 1px solid var(--ihr-grey-250);
  padding: 12px 20px; display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: baseline;
  /* Stays put while the page scrolls, so the width control is reachable from
     any card rather than only from the top. z-index 5 clears the drawers, which
     sit at 3 and 4 inside a card. */
  position: sticky; top: 0; z-index: 5;
}
.brand { font-weight: 600; }
.brand span { font-weight: 400; font-size: 12px; color: var(--ihr-grey-450); margin-left: 8px; }
.hint { font-size: 12px; color: var(--ihr-grey-450); }
/* Hiding the waveform on the artwork cards. .widget.hero covers design B and
   design C, since C carries both classes, and leaves the bar card alone.

   `display`, not `visibility`. Visibility kept the strip's box, which was the
   point at the time, but the card does not get shorter either way, so all that
   box did was leave the waveform's own height as dead space under the bottom
   row: 24 on design C, 12 on design B.

   Taking the box out entirely would push the row flat against the card's edge,
   so the strip's height is replaced by 16 of padding. Frame 2613:77793 draws
   design C without a waveform and leaves 17 under the row, and 16 is what the
   rest of the card is inset by, so the two agree. */
/* Keyed on any ancestor carrying the attribute, not on <main>. The prototype
   page sets it there; the single card page has no <main> and sets it on <body>,
   which is how the host page's slot reaches it. */
[data-wave="off"] .widget.hero .wave { display: none; }
/* 8, so the LOCKUP lands 16 above the card's edge rather than the row's box
   doing so. The row is held at 32 and the lockup is 16, so it centres with 8 of
   row underneath it; 8 of padding plus that 8 is the 16 that was asked for.
   The consequence is that the buttons, which fill the 32, now sit 8 from the
   edge rather than 16. */
[data-wave="off"] .widget.hero .hero-bottom { padding-bottom: var(--card-padb, 4px); }

/* The column toggle, same shape as the width control beside it. */
.colctl { display: flex; align-items: center; gap: 8px; }
.colctl label { font-size: 12px; color: var(--ihr-grey-450); }
.colctl .seg { display: inline-flex; border: 1px solid var(--ihr-grey-250);
               border-radius: 999px; overflow: hidden; background: #fff; }
.colctl .seg button { appearance: none; border: 0; background: transparent; cursor: pointer;
                      font: inherit; font-size: 12px; padding: 4px 12px;
                      color: var(--ihr-grey-450); }
.colctl .seg button[aria-pressed="true"] { background: var(--ihr-grey-600); color: #fff; }
.colctl .seg button:focus-visible { outline: 2px solid var(--ihr-red-550); outline-offset: -2px; }
.hint a { color: var(--ihr-red-600); }

main { padding: 26px 20px 80px; max-width: 1320px; margin: 0 auto; display: grid; gap: 34px; }

/* Two designs across, for comparing them rather than scrolling between them.

   The rows are placed explicitly rather than left to auto-flow. Auto-placement
   walks a cursor forward and will not go back, so design C's band would land
   under design A's live card instead of beside its own band. Pinning the three
   rows means the two headers sit level, podcast sits beside podcast and live
   beside live, which is the only arrangement that makes a side by side worth
   having. The shipping embeds stay full width above it all.

   The two cards in a row are different heights, design A's podcast card being
   400 against design C's 234, so the shorter one leaves space under it. That is
   the comparison being honest rather than a layout fault, which is why they are
   aligned to the top of the row and not stretched. */
@media (min-width: 900px) {
  main[data-cols="2"] { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
                        column-gap: 28px; align-items: start; }
  /* What ships on the left, the design on the right, with podcast beside
     podcast and live beside live. This used to stack the shipping block full
     width ABOVE two designs side by side, which was right while there were
     three sets. There are two now, so the pair being compared is what ships
     against one design, and they belong in a column each.

     Explicit rows, not auto-flow, for the reason the three column layout gave:
     auto-flow will not go back for a column it has already passed, so a section
     with no rule lands wherever there is space rather than where it belongs.

     The design side is selected by exclusion rather than by name, so whichever
     of A, B and C is uncommented takes the right hand column and no CSS has to
     change when they are swapped. Uncomment two and they stack there, which is
     a fair degradation rather than a break. */
  main[data-cols="2"] > .searchrow { grid-column: 1 / -1; grid-row: 1; }
  main[data-cols="2"] > [data-design="ship"] { grid-column: 1; }
  main[data-cols="2"] > [data-design]:not([data-design="ship"]) { grid-column: 2; }
  main[data-cols="2"] > .band[data-design] { grid-row: 2; }
  /* Live radio first, podcast second. The rows are explicit, so this order is
     set here and NOT by the document; the sections are ordered to match so the
     one column layout, which has no rules of its own and simply follows the
     document, reads the same way round. Change one and change the other. */
  main[data-cols="2"] > [data-design][data-kind="live"] { grid-row: 3; }
  main[data-cols="2"] > [data-design][data-kind="podcast"] { grid-row: 4; }
  /* The three shipping types with no design counterpart, stacked under the pair
     that has one. Rows 3 and 4 are already claimed, so these need rows of their
     own or they auto-flow into the design column. */
  /* Every content type now exists on BOTH sides, so each takes a row of its own
     and the two columns line up type against type all the way down. */
  main[data-cols="2"] > [data-design][data-kind="episode"] { grid-row: 5; }
  main[data-cols="2"] > [data-design][data-kind="artist"] { grid-row: 6; }
  main[data-cols="2"] > [data-design][data-kind="playlist"] { grid-row: 7; }
  /* The 404 example, which only the shipping side has. It takes a row of its
     own so it stays under playlist in column 1 rather than auto-flowing into
     the design column, and column 2 of that row is simply empty. */
  main[data-cols="2"] > [data-design][data-kind="notfound"] { grid-row: 8; }
  /* The other two error examples, same reasoning, rows of their own so they
     stay stacked under the 404 in column 1. */
  main[data-cols="2"] > [data-design][data-kind="noflag"] { grid-row: 9; }
  main[data-cols="2"] > [data-design][data-kind="wrongid"] { grid-row: 10; }
  main[data-cols="2"] > [data-design][data-kind="georegion"] { grid-row: 11; }
}

/* The three column option is gone. It existed to put what ships beside design
   B beside design C; with design B hidden there are two sets, so two columns is
   the widest arrangement the page has anything to put in. */

/* Below 900 the two columns would be narrower than the cards are designed for,
   so the toggle is simply ignored rather than disabled: the page falls back to
   one column and the button stays where it was. */
/* Anything scrolled to lands below the sticky bar rather than under it. */
section, .band { scroll-margin-top: 92px; }
section > h2 { font-size: 13px; margin: 0 0 2px; text-transform: uppercase; letter-spacing: .06em; }
section > p.sub { margin: 0 0 12px; font-size: 12.5px; color: var(--ihr-grey-450); max-width: 80ch; }

/* ---------- width gauge ----------
   The widget lays itself out from its OWN width, not the window's, so the
   readout reports the shell width (what the container query actually reads)
   next to the window width. They differ by the page gutters and the 1320
   max-width, which is exactly the thing that is otherwise invisible. */
.h2row { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 10px; margin: 0 0 2px; }
.h2row h2 { margin: 0; }
.gauge {
  font-size: 11.5px; letter-spacing: 0; text-transform: none; font-weight: 400;
  color: var(--ihr-grey-450); font-variant-numeric: tabular-nums;
  border: 1px solid var(--ihr-grey-250); border-radius: 999px; padding: 2px 9px; background: #fff;
}
.gauge b { font-weight: 600; color: var(--ihr-grey-600); }
.gauge .mode { font-weight: 600; }
.gauge.wide .mode { color: var(--ihr-red-550); }
.hint .vw { font-variant-numeric: tabular-nums; }

/* ---------- the width slider ---------- */
.widthctl { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ihr-grey-450); }
.widthctl label { white-space: nowrap; }
.widthctl input[type="range"] { width: 220px; accent-color: var(--ihr-red-550); cursor: pointer; }
.widthctl input[type="number"] { width: 68px; font: inherit; font-size: 12px; font-weight: 600;
  font-variant-numeric: tabular-nums; text-align: right; color: var(--ihr-grey-600);
  padding: 3px 6px; border: 1px solid var(--ihr-grey-250); border-radius: 6px; background: #fff; }
.widthctl input[type="number"]:focus { outline: 2px solid var(--ihr-red-550); outline-offset: -1px; }
.widthctl .unit { margin-left: -4px; }
.widthctl output { font-variant-numeric: tabular-nums; color: var(--ihr-grey-450); min-width: 34px; }
.widthctl button { font: inherit; font-size: 11px; padding: 3px 9px; border-radius: 999px;
                   border: 1px solid var(--ihr-grey-250); background: #fff; color: var(--ihr-grey-600);
                   cursor: pointer; }
.widthctl button:hover { background: var(--ihr-grey-150); }

/* ---------- what is live, what is not ----------
   The page carries the production player and four prototypes at once, which is
   the whole point of it and also the easiest thing in the world to misread in a
   screenshot. Every group is banded and every card is tagged, so a section
   cannot be looked at without its status being on screen next to it. */
.band { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px;
        padding: 14px 16px; border-radius: 10px; margin: 0 0 4px; }
.band p { margin: 0; font-size: 13px; max-width: 90ch; }
.band-live { background: #e7f4ec; border: 1px solid #bcdfc9; color: #14532d; }
.band-proto { background: #fff4e2; border: 1px solid #f0d5a8; color: #6b4708; }
/* What ships today reads as a control rather than a prototype. */
.band-ship { background: #e9eef2; border: 1px solid #ccd6de; color: #2c3e4c; }
.band h2 { font-size: 15px; margin: 0; }
.tag { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
       padding: 3px 8px; border-radius: 999px; white-space: nowrap; }
.tag-live { background: #12703c; color: #fff; }
.tag-proto { background: #8a5a06; color: #fff; }
/* The same two tags again on every individual card, since a section can be
   scrolled to, screenshotted or linked on its own. */
.h2row .tag { font-weight: 700; }
section.is-live > .band-note { color: #14532d; }

/* ---------- the shipping player, for comparison ---------- */
/* Stacked, never side by side. Two columns halved the width each embed got,
   which is the one thing a player's layout actually responds to, so the
   baseline was being shown in a form the page never asked about. Each one now
   takes the full width, the same as the prototype cards below it. */
.embeds { display: grid; gap: 16px; grid-template-columns: 1fr; }
.embed { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.embed-label { font-size: 12px; color: var(--ihr-grey-450);
               overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* What this frame is currently showing. The embed has always followed a search;
   this is how you can tell, since the frame itself redraws to a player that
   looks much the same whatever is in it. */
.embed-now { color: var(--ihr-grey-600); font-weight: 600; }
/* The bullet went with the words. It separated "iHeart embed" from the name,
   and with nothing before it, it was just a dot hanging off the left. */
.embed iframe { width: 100%; height: 200px; border: 0; display: block;
                border-radius: var(--radius-widget); background: var(--ihr-grey-150); }
/* iHeart's own published heights, from its oEmbed endpoint: 300 for the show,
   artist radio and playlist, 200 for an episode and live radio, width 100% on
   all five. These were 440 and 200, where 440 was design B's card height
   rounded down by 4 so the two would line up in a column. That was a reasonable
   choice while nothing better existed and it is the wrong one now: the point of
   this section is what a publisher actually gets, and a publisher gets what the
   embed snippet says. */
[data-design="ship"][data-kind="podcast"] .embed iframe { height: 300px; }
/* Artist radio and playlist carry a "Featured artists in this playlist" list
   under the transport, which is the same shape of thing the show's episode list
   is, so they take the same 440 and the whole column reads at one height.

   Measured at 411 wide, the natural content runs to 517 on the show, 581 on
   artist radio and 481 on playlist, so 440 clips artist by 141 and playlist by
   41. That is left alone deliberately. The shipping widget clips at every
   height anyway, which is the finding this section exists to show, and picking
   a different height per type would trade a real behaviour for a tidy column.

   The episode frame keeps the base 200. Its content measured exactly 200 at
   411 wide, so it is the one type here with nothing below the fold to reveal,
   and 440 would give it 240 of empty grey. */
[data-design="ship"][data-kind="artist"] .embed iframe,
[data-design="ship"][data-kind="playlist"] .embed iframe { height: 300px; }
/* 300 so these two sit at the same height as the podcast embed above them and
   the comparison is like for like. The no-flag frame holds a 5232px document
   and will scroll inside those 300; that IS the demonstration, so it is not
   given room to look better than it is. */
[data-design="ship"][data-kind="noflag"] .embed iframe,
[data-design="ship"][data-kind="wrongid"] .embed iframe { height: 300px; }

/* The Auto button is a state, not an action, so it reads as pressed while no
   height is set rather than looking like a button you have yet to use. */
.heightctl button[aria-pressed="true"] { background: var(--ihr-grey-600); color: #fff; }

/* ---------- artist radio and playlist, Design D ----------
   Three additions and nothing else. These cards are design C's card with a
   different transport, so everything structural is already above. */
/* The artist radio frame draws a circular artist image where the other cards
   draw a rounded square. Playlist keeps the square. */
.topbar .h-thumb.round { border-radius: 50%; }
/* The free user skip allowance used to be a separate badge pinned to this
   button's corner. The supplied asset draws the count into the glyph itself, so
   the badge would have been a second 6 beside the first. */
/* A featured artist row selects nothing, so it gets no hover, no pointer and no
   room reserved for an overflow button. */
.list .row.inert { cursor: default; padding-right: 16px; }
.list .row.inert:hover { background: transparent; }
/* A featured artist row is an anchor now, opening that artist on iHeart, so it
   takes the pointer and the hover back. Scoped to [href] so a row the catalog
   gave no slug still reads as inert rather than advertising a link it does not
   have. Colour and underline are reset because an anchor brings its own. */
a.row.inert { color: inherit; text-decoration: none; }
.list a.row.inert[href], a.row.inert[href] { cursor: pointer; }
.list a.row.inert[href]:hover, a.row.inert[href]:hover { background: rgba(0,0,0,.05); }

/* ---------- the tail of a list, Design D 2666:126249 ----------
   A link out to the rest, then the legal line. */
/* The icon sits at the ROW's right edge, in line with the overflow buttons on
   the episode rows above, which is where the frame puts it. It was tucked
   against the end of the label instead, 110px short of them.

   16 of right padding, not 12, and that is arithmetic rather than taste. The
   overflow button is `right: 12px` and 32 wide with 4px of its own padding, so
   its 24px glyph starts 16 in from the row's edge. Matching that number here
   lines the two glyphs up exactly. */
/* .row.row-more-link, not .row-more-link. The base .row reserves 52px on its
   right for the absolutely positioned overflow button, and at equal specificity
   that padding won: the glyph landed at the edge of that reservation, 36px
   short of the overflow glyphs it is meant to line up with. This row has no
   overflow button, so it has nothing to reserve. */
.row.row-more-link { display: flex; align-items: center; gap: 12px; text-decoration: none;
                     padding: 8px 16px 8px 12px; border-radius: var(--rad-6); }
.row.row-more-link .row-text { flex: 1 1 auto; min-width: 0; }
.row-more-link .row-title { font-size: var(--fs-14); line-height: var(--lh-18);
                            font-weight: 600; letter-spacing: var(--ls-2);
                            color: var(--ihr-grey-600); }
.row-more-link:hover { background: var(--ihr-grey-150); }
.row-more-link .row-out { width: 24px; height: 24px; flex: 0 0 auto; display: block; }
/* Caption 4, not the frame's Body 3.

   The frame draws this at 16 / 24 at -0.5, which is body copy, and body copy is
   the wrong register for a footnote that sits under every list on every card.
   accomplice's caption-4 is 12 / 400 / 16 at letterSpacing 4, the kind the
   system keeps for exactly this, and .k-caption-4 above already carries it.
   Deliberate departure from the frame, asked for.

   Centred, with the links in the system's link blue and the pipe left in body
   colour rather than tinted along with them. */
.list-legal { margin: 8px 0 12px; text-align: center;
              font-size: var(--fs-12); line-height: var(--lh-16);
              font-weight: 400; letter-spacing: var(--ls-4);
              color: var(--ihr-grey-600); }
.list-legal a { color: #0055b7; text-decoration: none; }
.list-legal a:hover { text-decoration: underline; }
.list-legal span { margin: 0 6px; }
/* The tail scrolls with the rows, so the scroll area is the whole list. */
.list .rows, .sheet-body .rows { flex: 1 1 auto; min-height: 0; }
/* It is the last child of a 2px gap column, so it carries its own breathing
   room rather than inheriting the rhythm of the rows above it. 16 below, so the
   list does not end flush against the bottom of the drawer.
   The rows are a flex column, so this is a real gap rather than a margin that
   collapses away or gets ignored at the end of a scroll container. */
.rows > .list-legal { margin: 12px 0 16px; }

/* ---------- drawer scrollbars ----------
   Shown whenever there is overflow, rather than left to the platform to fade in
   on the first scroll. macOS hides overlay scrollbars until you touch them, so
   on a trackpad a drawer with six more rows below the fold looked like a drawer
   with three rows in it. The Design D drawers draw a Scroll Bar instance for
   exactly this reason.

   Styling ::-webkit-scrollbar at an explicit width opts that element out of
   overlay scrollbars entirely, so it is present and visible whenever the
   content overflows and absent when it does not. */
/* The rows keep their own scrolling; the bar is painted beside them and never
   takes the pointer, so a drag still lands on a row. */
.list, .sheet-body { position: relative; }
.rows { scrollbar-width: none; }
.rows::-webkit-scrollbar { width: 0; height: 0; }
.rows-bar {
  position: absolute; right: 4px; width: 4px; border-radius: 10px;
  background: var(--ihr-grey-250); pointer-events: none; z-index: 2;
}
.rows-thumb {
  position: absolute; left: 0; width: 100%; border-radius: 10px;
  /* gray, not grey. This file carries both spellings and only --ihr-gray-500
     exists; --ihr-grey-500 resolved to nothing, so the thumb was transparent
     and the bar looked like a track with no thumb in it, which is exactly what
     it was. */
  background: var(--ihr-gray-500);
}
/* Firefox has no pseudo elements for this and needs the standard properties,
   but they cannot simply sit alongside: once scrollbar-width is specified,
   Chrome ignores every ::-webkit-scrollbar rule above and falls back to its
   overlay scrollbar, which is the thing being fixed. Measured, the track took
   zero layout width with both declared and 8px with only the pseudo elements.
   So they are given only to engines that have no pseudo elements to ignore. */


/* ---------- search ---------- */
/* The two page level search boxes, side by side above everything and spanning
   every column in the multi column layouts. */
/* Five boxes now, one per content type. auto-fit rather than a fixed count, so
   they wrap to whatever the page can give them instead of needing a breakpoint
   per column count. 280 is the narrowest a placeholder naming two examples
   stays readable at. */
.searchrow { display: grid; gap: 16px;
             grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
.searchrow .search { margin-bottom: 0; max-width: none; }
.search { position: relative; margin-bottom: 12px; max-width: 460px; }
.search input {
  width: 100%; font: inherit; font-size: 14px; padding: 9px 12px;
  border: 1px solid var(--ihr-grey-250); border-radius: 8px; background: #fff; color: inherit;
}
.search input:focus { outline: 2px solid var(--ihr-red-550); outline-offset: -1px; }
.results {
  position: absolute; z-index: 20; left: 0; right: 0; top: calc(100% + 4px);
  background: #fff; border: 1px solid var(--ihr-grey-250); border-radius: 8px;
  box-shadow: 0 8px 24px rgba(0,0,0,.14); max-height: 320px; overflow-y: auto; padding: 4px;
}
.results[hidden] { display: none; }
.results button {
  display: flex; gap: 10px; align-items: center; width: 100%; text-align: left;
  background: none; border: 0; font: inherit; padding: 8px; border-radius: 6px; cursor: pointer;
}
.results button:hover, .results button:focus { background: var(--ihr-grey-150); outline: none; }
.results img { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; background: var(--ihr-grey-250); flex: 0 0 auto; }
.results b { display: block; font-size: 13px; font-weight: 600; }
.results span { display: block; font-size: 11.5px; color: var(--ihr-grey-450); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.results .empty { padding: 12px; font-size: 12.5px; color: var(--ihr-grey-450); }

/* ============================================================================
   WIDGET. One set of rules; the container query does the rest.
   ==========================================================================*/
/* Every player is capped by one variable, so the slider moves all of them at
   once: the four prototypes and the shipping embeds above them. Unset means no
   cap, which is the page filling the window as it did before. */
.shell { container-type: inline-size; width: 100%; max-width: var(--player-w, none); }
/* On .embed, not .embeds. The width control caps every player through this one
   custom property, and the shipping frames used to inherit it from the wrapper
   that held both of them side by side. Splitting that block into one section
   per content type deleted the wrapper, so the rule stopped matching anything
   and the shipping widgets quietly stopped answering the slider while the
   prototypes kept doing so. */
.embed { max-width: var(--player-w, none); }
.widget { position: relative;
  border-radius: var(--radius-widget); overflow: hidden; width: 100%;
  position: relative;                      /* the info drawer covers the card */
  display: flex; flex-direction: column; box-shadow: 0 2px 10px rgba(0,0,0,.10);
}
.player {
  width: 100%; min-height: 180px; position: relative;
  display: flex; flex-direction: column; justify-content: space-between; padding-top: 16px;
  /* artwork colour with the design's 70% black on top, authored as the same
     two stacked gradients Figma uses */
  background-image:
    linear-gradient(90deg, var(--overlay) 0%, var(--overlay) 100%),
    linear-gradient(90deg, var(--dominant, #191b28) 0%, var(--dominant, #191b28) 100%);
  transition: background-image .35s ease;
}
.body {
  display: grid; width: 100%; padding: 0 16px; position: relative;
  grid-template-columns: 64px 1fr;
  grid-template-areas: "art meta" "controls controls";
  column-gap: 12px; row-gap: 16px; align-items: center;
}
/* The artwork and the metadata are links to the thing that is playing. The
   anchor takes the grid slot so the image keeps its own box, and it inherits
   colour and loses the underline so the card looks exactly as it did. */
.art-link { grid-area: art; align-self: center; display: block; line-height: 0; border-radius: var(--radius-widget); }
.art { width: 64px; height: 64px; border-radius: var(--radius-widget);
       box-shadow: var(--shadow-01); object-fit: cover; background: #222; }
/* Colour is never touched on a text link. Setting `color: inherit` on the
   metadata block once outranked the .meta rule that paints the header text
   grey-100, and every line on the bar card turned near black. The image links
   can inherit safely, since they carry no text. */
a.line-link, a.thumb-link, a.art-link { text-decoration: none; }
a.thumb-link, a.art-link { color: inherit; }
a.line-link { color: inherit; }
/* The underline goes on the SPAN, not only on the anchor. A hovered line that
   overflows turns its span into an inline-block so the marquee can translate it,
   and an inline-block does not draw its ancestor's text decoration, so the
   underline vanished at the very moment it was asked for. Declaring it on both
   covers the lines that marquee and the lines that do not. */
/* accomplice Link, the `underline: hover` variant plus its focus treatment.
   The component draws focus as a 1px solid outline at 1px offset with
   radius[2], which is tighter than the 2px ring this page used. */
a.line-link:hover, a.line-link:hover .mqi {
  text-decoration: underline; text-underline-offset: 2px;
}
a.line-link:focus-visible, a.thumb-link:focus-visible, a.art-link:focus-visible {
  outline: 1px solid var(--true-white); outline-offset: 1px;
  border-radius: var(--rad-2); text-decoration: none;
}
/* The text lines INSET their ring instead. Every one of them sits inside a
   marquee, which is overflow:hidden so the text can ellipsise, and the link
   fills that box exactly: measured, zero pixels to the left and minus one at
   the top. A ring drawn at +1 offset has nowhere to go and gets cut on two
   sides. Drawing it at -1 puts it inside the box the clip respects, which is
   the same technique the episode rows already use at -2. The component's +1 is
   kept for the artwork links, which are not inside a clip. */
a.line-link:focus-visible { outline-offset: -1px; }
a.thumb-link { flex: 0 0 auto; display: block; line-height: 0; border-radius: var(--radius-widget); }
/* The LINK is the square, and the image is taken out of the layout inside it.

   Sizing the image directly was circular and blew up to 3000px: stretching it
   to the flex line made the line's height depend on the image, and the image's
   own intrinsic height fed straight back into it. Absolutely positioning the
   image means it contributes nothing, so the line's height comes from the text
   alone, the link stretches to it, and aspect-ratio gives the width. */
/* Sized from --thumb, which fitThumb() measures off the text block.

   Pure CSS could not do this reliably. align-self: stretch with aspect-ratio
   was tried and failed twice: sizing the image directly was circular and gave
   3000px tiles, and sizing the link gave 32x34 because a min-height resolved
   its width before the stretch, then 6x34 once the shell became a size
   container. The height is only definite after the stretch, and the width has
   to be resolved before it, so one measurement in JS is the honest answer. */
.topbar a.thumb-link { width: var(--thumb, 48px); height: var(--thumb, 48px);
                       position: relative; overflow: hidden; }
/* 36 floor, the podcast card's own two line height, so every card in a rail
   shows the same tile and only a taller than usual block grows it. */
.topbar .meta { min-height: 36px; justify-content: center; }
a.thumb-link:hover { opacity: .92; }
/* Station logos are transparent PNGs drawn for a white tile. Left on the dark
   header the artwork bleeds into the background and the mark loses its edges,
   so live radio gets the white plate the design gives it. */
.widget.live .art { background: #fff; object-fit: contain; padding: 4px; }
.widget.live .tile { background: #fff; background-image: none; object-fit: contain; padding: 3px; }
/* The text and the controls are one group in the live frames, so they get a
   wrapper. Everywhere except live-wide the wrapper is display:contents, which
   removes it from the box tree and leaves .meta and .controls-row as the grid
   items they were. */
.col { display: contents; }
/* 2px for live, which is what all three revised frames draw between the three
   lines. Podcast keeps 4 between its pair. */
.widget.live .meta { gap: 2px; }
/* Two lines rather than three, and frame 2600:92969 sets them 4 apart: the
   block is 38 tall for an 18 and a 16, which only adds up at 4. Three lines
   under a track keep the 2 their own frame draws. */
.widget.live.notrack .meta { gap: 4px; }
/* 33px is what holds the lines off the logo, and it is the only thing that
   does on any of the three designs: the logo is 25 wide and sits 16 in from the
   card's right edge, so 33 of text padding against the same edge leaves 8 of
   clearance. Every card takes 16 on that edge, so the one number serves all. */
.meta { grid-area: meta; display: flex; flex-direction: column; min-width: 0; gap: 4px;
        padding-right: 33px; color: var(--ihr-grey-100); }
/* ---- accomplice Text kinds, from components/text/kind.ts ----
   Declared once and applied by name, rather than the same four numbers
   retyped at every call site. The mapping is exact, not approximate: what the
   Figma frames drew already WAS the system, so every line in these cards lands
   on a real kind.
     subtitle-4  14 / 18 / 600 / -0.2   the podcast title, the artist line
     caption-2   14 / 18 / 400 / -0.2   the track line
     caption-4   12 / 16 / 400 /  0     the show line, the station line
     caption-3   12 / 16 / 600 / -0.2
     button-2    14 / 16 / 600 / -0.2   the speed control
     overline-1  11 / 16 / 700 /  1     uppercase, the explicit badge  */
.k-subtitle-4 { font-size: var(--fs-14); line-height: var(--lh-18);
                font-weight: 600; letter-spacing: var(--ls-0); }
.k-caption-2  { font-size: var(--fs-14); line-height: var(--lh-18);
                font-weight: 400; letter-spacing: var(--ls-0); }
.k-caption-3  { font-size: var(--fs-12); line-height: var(--lh-16);
                font-weight: 600; letter-spacing: var(--ls-0); }
.k-caption-4  { font-size: var(--fs-12); line-height: var(--lh-16);
                font-weight: 400; letter-spacing: var(--ls-4); }
.k-button-2   { font-size: var(--fs-14); line-height: var(--lh-16);
                font-weight: 600; letter-spacing: var(--ls-0); }
.k-overline-1 { font-size: var(--fs-11); line-height: var(--lh-16);
                font-weight: 700; letter-spacing: var(--ls-5); text-transform: uppercase; }

.title { font-size: var(--fs-14); line-height: var(--lh-18); font-weight: 600;
         letter-spacing: var(--ls-0); margin: 0;
         overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Live radio shows now playing over two lines, track then artist, with the
   station line beneath. Track is Regular and artist is SemiBold, which is the
   opposite weighting to the podcast title, straight from the frame. */
/* The two now-playing lines are siblings of the station line rather than a
   nested block, since the revised frames put all three in one column 2px apart.
   The weight rules move with them: scoped to .titles they stopped applying the
   moment the wrapper went, and the track quietly rendered SemiBold. */
.np-track { font-weight: 400; }
.np-artist { font-weight: 600; }
.widget.live .title { font-size: var(--fs-14); line-height: var(--lh-18); letter-spacing: var(--ls-0); }
/* caption-4 */
.subtitle { font-size: var(--fs-12); line-height: var(--lh-16); letter-spacing: var(--ls-4);
            margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Nothing here for the no-track state any more. It used to bump this single
   joined line to the title's size, because the station line was the only line
   in the block and 12/16 read as a caption with nothing above it. Frame
   2600:92969 answers it properly instead: with nothing on air the block splits
   into the station NAME on the title line and its description underneath, so
   each line is already the right size and the override has nothing left to do. */
/* accomplice Logo sizes the mark by HEIGHT off a fixed scale, and 26 is not on
   it: the scale runs 12, 16, 18, 24, 32, 36, 40, 48, 52, 56 and up. 24 is the
   nearest rung, so the mark is 24 tall here and the width follows its ratio
   rather than being pinned. This is the one place the port changes a drawn
   size, and it is a 2px difference. */
.ihr { height: var(--fs-24); width: auto; display: block; }
/* The logo is a link home, so the ANCHOR carries the position rather than the
   image. Leaving the absolute positioning on the image left the anchor with a
   zero sized box in the flow, which is a link nobody can click. */
.ihr-link { position: absolute; right: 16px; top: 0; display: block; line-height: 0; color: inherit; }
/* The artwork cards park the logo in the card's top right corner, the same
   place the bar card puts it, rather than letting it ride the top bar's flex
   row and drift down as the text block grows. The top bar is the containing
   block and spans the full width, so right 16 and top 0 land 16 from the card's
   right edge and 16 from its top, which is exactly the bar card's inset. */
.topbar .ihr-link { right: 16px; top: 0; }
/* The text already clears the logo on its own: .meta carries 40px of right
   padding, which is the logo's 25 plus 15 of slack, and that is the whole of
   what keeps design A's lines off it. So the top bar takes the same 16px the
   bar card's .body takes, and all three designs truncate at the same point,
   56px in from the card's right edge.
   The whole bar is 16 now, both sides, so this override says nothing the base
   rule does not. Kept only as the note above; the declaration is gone. */
.ihr-link:hover { opacity: .85; }
.ihr-link:focus-visible { outline: 2px solid var(--true-white); outline-offset: 2px; border-radius: 4px; }

/* ============================================================================
   HERO CARD. Frames 2512:113087 (podcast) and 2512:111965 (live radio).
   The artwork is the background, at any width, under the frames' own 70% black.
   Only the 350 frame exists for this design, so the stage holds that frame's
   224px height rather than inventing a taller one for wide viewports.
   ==========================================================================*/
.widget.hero { border-radius: var(--radius-widget); overflow: hidden; width: 100%;
               display: flex; flex-direction: column; box-shadow: 0 2px 10px rgba(0,0,0,.10); }
/* The whole card is a play target, so it says so. */
.widget.hero .stage { cursor: pointer; }
.widget.hero .sheet, .widget.hero .slider { cursor: auto; }
.stage { position: relative; height: 224px; width: 100%; overflow: hidden;
         display: flex; flex-direction: column; justify-content: space-between;
         padding-top: var(--card-pad, 8px); background: var(--stage-plate); }
/* The plate under the artwork is a THEME colour, not something read off the
   picture. It was the artwork's own mean colour for live radio for a while:
   a station's logo is a transparent PNG drawn for a white tile, so a white
   plate shows through wherever the logo does not cover, and that read as a flat
   grey rather than as the station.

   The frames disagree, and they are unambiguous about it. #333333 is the single
   most common colour in both live frames, 48,640 of 78,400 pixels on 2512:111965
   and 59,904 of 92,050 on 2512:111876, and #333333 is exactly what a white plate
   becomes under 80% black. The logo's own colours composite over it at 20%,
   which is where 2512:111965's #2f001c comes from: the Z100 pink at a fifth.

   So the plate is white, and every live card reads the same rather than each
   taking its station's colour. Dark theme swaps --stage-plate and nothing else. */
/* background and padding are reset explicitly. The other design gives a live
   station's .art a white plate and 4px of padding, which is right for a 64px
   tile and wrong for a full bleed background: the white plate is what made the
   live card render as flat grey under the scrim rather than taking the
   station's colour. */
.widget.hero .stage > .art { position: absolute; inset: 0; width: 100%; height: 100%;
                object-fit: cover; border-radius: 0; box-shadow: none;
                background: transparent; padding: 0; }
/* 80, not the frames' 70. A deliberate step past them on both artwork designs,
   asked for after looking at the two side by side. */
.scrim { position: absolute; inset: 0; background: rgba(0,0,0,.80); }
/* 16 across, not 24. It was the one place on the artwork cards that was not
   16: the slider, the bottom row and the episode rows were all 16 already, and
   the top bar's own right side had been brought to 16 for the logo, so the left
   was the last 24 on the card. */
.topbar { position: relative; display: flex; align-items: center; gap: 8px;
          padding: 0 var(--card-pad, 8px); width: 100%; }
/* h-thumb, not thumb. `.thumb` is already the slider's drag handle, which is
   position:absolute, so reusing the name took this image out of the flex row
   and let the two lines of text run underneath it. Second time a class name
   from the other design has leaked in; the h- prefix keeps this card's names
   to itself. */
/* The artwork is square and as tall as the text beside it, rather than a fixed
   48. align-self: stretch makes it the height of the flex line, aspect-ratio
   makes the width follow, and the text block is the tallest thing in the row so
   it is the text that decides.

   This is what was flooring the top bar. At a fixed 48 the bar could never be
   shorter than the artwork however little text it held, and on live radio's old
   three line block the text was taller still, which is why compressing the
   thumbnail did nothing there. Now the bar is exactly as tall as its content.

   min-height on the LINK rather than the image, because the image is the thing
   being stretched; without a floor a one line card would draw a very small
   tile, and 32 matches the icon buttons at the other end of the row. */
.topbar .h-thumb { border-radius: var(--radius-widget); object-fit: cover;
                   background: #fff; display: block;
                   position: absolute; inset: 0; width: 100%; height: 100%; }
/* Station logos are transparent PNGs drawn for a white tile, exactly as in the
   other widget, and the live frame shows the thumb on white. */
.widget.hero.live .topbar .h-thumb { object-fit: contain; padding: 3px; }
.topbar .meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1 1 0; }
/* Design C's top bar reserves no room for a logo any more, so the 33px that
   held the lines off one is dead weight here.

   `.meta` carries `padding-right: 33px` because the iHeart badge is 25 wide and
   sits 16 in from the card's edge, so 33 of text padding leaves 8 of clearance
   from it. That is still true for design A and design B, which both still have
   the badge sitting absolutely in that corner with nothing reserving its space.

   Design C replaced the badge with a real share BUTTON, a flex child that
   reserves its own 32 and is separated by the row's own gap. The 33 was still
   being applied on top of all that, so the text truncated 89px from the card's
   right edge when the button only needs 56. */
.widget.c .topbar .meta { padding-right: 0; }
/* The live block runs flush, no gap between its three lines. The frames carry
   2px there, this is a deliberate step past them, and it applies to the two
   artwork cards only: design A's live block and both podcast pairs keep the 2. */
.widget.hero.live .topbar .meta { gap: 0; }
/* The artwork cards restated the drawn 25x26. On the Logo scale that rung is
   24, and it is set once on .ihr, so this override has nothing left to say. */
.topbar .ihr { height: var(--fs-24); width: auto; }
/* Two type styles, swapped between the two frames. The name line is the
   SemiBold one at 14/18, the sub line the Regular one at 12/16. */
/* The live block, identical across all three designs after the revision:
   station line 12/16 Regular, artist 14/18 SemiBold, track 14/18 Regular, every
   line in grey-100 and 2px apart. The station line is NOT white and NOT dimmed,
   it is the same grey-100 as the two above it, since the whole column carries
   that colour in the frames. */
.h-station { margin: 0; font-size: var(--fs-12); line-height: var(--lh-16);
             font-weight: 400; letter-spacing: var(--ls-4);
             color: var(--ihr-grey-100); }
.h-name { margin: 0; font-size: var(--fs-14); line-height: var(--lh-18);
          letter-spacing: var(--ls-0); font-weight: 600;
          color: var(--ihr-grey-100); }
/* The bullet between the track and the artist. Dimmed, so the separator reads
   as punctuation rather than as a third piece of information. */
.np-sep { color: var(--ihr-grey-250); }
.h-under { margin: 0; font-size: var(--fs-14); line-height: var(--lh-18);
           letter-spacing: var(--ls-0); font-weight: 400;
           color: var(--ihr-grey-100); }
/* The podcast pair, off 2548:250303 (design B) and 2548:250299 (design C),
   which are identical. The episode leads at 14/18 SemiBold in TRUE WHITE and
   the show sits under it at 12/16 Regular in grey-100, which is the reverse of
   the order and the opposite of the colour pairing an earlier build had.
   Note the live block runs the other way on colour, every line grey-100. */
.h-ep { margin: 0; font-size: var(--fs-14); line-height: var(--lh-18);
        letter-spacing: var(--ls-0); font-weight: 600;
        color: var(--true-white); }
.h-show { margin: 0; font-size: var(--fs-12); line-height: var(--lh-16);
          font-weight: 400; letter-spacing: var(--ls-4);
          color: var(--ihr-grey-100); }
/* Still carries the live fallback's second line, the station description. */
.h-sub { margin: 0; font-size: var(--fs-12); line-height: var(--lh-16);
         font-weight: 400; letter-spacing: var(--ls-4); color: var(--true-white); }
/* The control row, frames 2512:113298 (podcast) and 2512:111876 (live).
   The row is the thing that is centred on the card, not the button: it is
   absolutely placed at the middle and lays its buttons out around the red one
   with an 8px gap. Idle hides every secondary button, which leaves the red one
   alone in the centre and reproduces the resting frames exactly. */
.hero-controls {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  display: flex; align-items: center; justify-content: center; gap: 8px;
  width: 100%; padding: 0 var(--card-pad, 8px); box-sizing: border-box;
}
/* Design C's centre row is a three column grid rather than a centred flex row,
   so the play button is centred on the CARD rather than on whatever happens to
   be beside it. A flex row centres the group, and design C's group is lopsided:
   speed and back to the left of the button, forward alone to the right. That
   put the button right of centre the moment playback revealed them, and it
   moved again whenever the set changed.
   `1fr auto 1fr` fixes the two sides to the same width whatever they hold,
   including nothing, so live radio's lone button lands in exactly the same
   place as the podcast card's. Design B keeps the flex row: its group is
   symmetrical by construction, three each side. */
.widget.c .hero-controls { display: grid; grid-template-columns: 1fr auto 1fr; }
.widget.c .cc-side { display: flex; align-items: center; gap: 8px; min-width: 0; }
.widget.c .cc-side:first-child { justify-content: flex-end; }
.widget.c .cc-side:last-child { justify-content: flex-start; }
.widget.hero.live .hero-controls { top: calc(50% - 1px); }
.h-btn { appearance: none; border: 0; background: transparent; cursor: pointer;
         padding: 4px; border-radius: 999px; display: flex; flex: 0 0 auto; }
.h-btn img { width: 24px; height: 24px; display: block; }
/* Design C's skip controls are drawn larger than the rest of its icons: 40
   rather than 24. Scoped to those two buttons on that design only, so design B
   and design C's own action row keep the 24 they share with every other icon
   button. The 4px of Button padding still applies, so the button itself grows
   to 48 and the grid either side of the play button absorbs it. */
.widget.c .hero-controls .h-btn[data-act="back"] img,
.widget.c .hero-controls .h-btn[data-act="fwd"] img { width: 40px; height: 40px; }

/* Skip next now takes the same 40 as its neighbours, because the supplied asset
   is drawn for it: a 40 viewBox with the arrow inset to the middle 16 and the
   skip count sitting above it. The margin is inside the artwork, so shrinking
   the element as well would have shrunk it twice. That is what the 28 here was
   compensating for, back when the glyph was a 16 box filled edge to edge.

   It was also briefly folded into the selector group above, which quietly took
   back and forward down to 28 too and left them 36px buttons beside a 48px 1x. */
.widget.c .hero-controls .h-btn[data-act="next"] img { width: 40px; height: 40px; }
/* The skip allowance, drawn OVER the glyph rather than inside it.

   It used to be part of the artwork: the supplied asset had a superscript 6 in
   its path, so the number was a picture and could never tick down. Production
   does not do that either. apps/listen's Next control renders a plain
   SkipForward and lays the count over it as its own caption-3 Text, which is
   exactly why theirs counts.

   caption-3 is 12 / 600 / 16, tied to --ctl so it steps down with the button it
   sits on instead of overflowing a 24px glyph at the bottom of the range. */
.widget.c .h-btn.skip { position: relative; }
.skip-n {
  position: absolute; left: 50%; top: 0; transform: translateX(-50%);
  z-index: 1; pointer-events: none;
  color: var(--true-white); font-weight: 600; letter-spacing: var(--ls-0);
  font-size: round(clamp(9px, var(--ctl, 40px) * 0.3, 12px), 1px);
  line-height: round(clamp(12px, var(--ctl, 40px) * 0.4, 16px), 1px);
}

/* 1x sized to the buttons it now sits beside.

   This is a deliberate departure from production, which is documented just
   below: PlaybackSpeed keeps a FIXED 32px box and drops the type rather than
   letting the button grow, because at 32 it has to. That reasoning was written
   while 1x sat in the bottom bar among 32px icons. It has moved into the
   control row beside back 15 and forward 30, which are 48, and a 32 box next to
   two 48s reads as a mistake rather than as restraint.

   40 plus the 4px of Button padding is 48, the same arithmetic the other two
   make. The type steps with the box, which is production's own rule rather than
   an invention: PlaybackSpeed sets caption-3 at 12 on small screens and steps
   up to caption-1 at 14 when there is room. Measured at 14 SemiBold the widest
   label, 1.25x, is 34.5, so a 40 box holds every step of the cycle. */
.widget.c .hero-controls .h-btn[data-act="speed"] { padding: 4px; }
.widget.c .hero-controls .h-speed { width: 40px; height: 40px; line-height: 40px;
                                    font-size: var(--fs-14); }
/* The Figma component in this slot is named Web Internal / Podcast Playback
   Speed, but the asset it exports is a play triangle, its own root being
   <g id="Web Internal / Play">. What the frame RENDERS there is the text 1x, so
   that is what is built, in the Caption 1 style the frame lists, 14/18 SemiBold
   at -0.4. Exported assets are the rule; an export that disagrees with the
   frame it came from is not one. */
/* button-2, like the bar card's speed control. The -0.4 tracking it carried
   is not a value the system has; the kind's is -0.2. */
/* The speed control, sized the way iheart.com sizes it. Production keeps the
   box FIXED and drops the type rather than letting the button grow: its
   PlaybackSpeed puts the label in a 3.2rem square and sets caption-3 on small
   screens, 12 / 16 / 600, stepping up to caption-1 at 14 only when there is
   room. Measured at 12px SemiBold the widest label, 1.25x, is 29.5, so a 32px
   box holds every step of the cycle with room to spare. At 14 it needed 34.5
   and nothing under 32 could hold it.

   The one departure: production pairs that 32px box with Button size `icon`,
   whose 4px of padding makes the button 40 overall, wider than its neighbours.
   Here the padding is dropped instead and the 32px box IS the button, so it
   matches the icon buttons beside it exactly and the row stays even. */
/* The spacer belongs in here too, and leaving it out moved the play button.
   It stands in for the speed button on live radio, and it holds the same 32px
   .h-speed box, but with no data-act it kept .h-btn's 4px padding and came out
   40 wide against the real button's 32. That is 8 more on the left of the play
   button than on the right, which is why the red button sat 4px right of centre
   the moment the row appeared. Measured: design B live drifted 210 to 214 on
   play, podcast drifted 0. */
.h-btn[data-act="speed"], .icon-btn[data-act="speed"],
.h-btn.spacer { aspect-ratio: auto; padding: 0; }
.h-speed { display: block; width: 32px; height: 32px; line-height: 32px;
           text-align: center; white-space: nowrap;
           font-size: var(--fs-12); font-weight: 600;
           letter-spacing: var(--ls-0); color: var(--true-white); }
/* Same variant as the bar card's icon buttons: tertiary / white. */
.h-btn:hover, .h-btn:active, .h-btn[aria-pressed="true"] {
  background-color: var(--ihr-gray-400);
}
/* Every artwork card now reveals its controls on play, design C included. Its
   own idle frame, 2533:85718, agrees: that card is drawn with the top bar, one
   centred play button and the waveform, no control row, no scrubber and no list
   button. An earlier build read C's live frame, which is drawn idle with its
   icons showing, and generalised from it. */
/* `started`, not `playing`. The controls appear on the first play and then
   stay, including while paused: a card you have played is no longer pristine,
   and hiding its transport every time you pause it makes the controls feel
   like they are fleeing the pointer. Only the untouched card is bare.
   Scoped to .hero-controls, which is the transport. Design C's actions moved
   out of that row into the bottom right one, and both of its frames draw all
   of them on a card nobody has played yet, so those stay put. Design B keeps
   everything in .hero-controls and is unaffected. This replaces an exclusion
   for the list button, which the scoping now covers on its own. */
.widget.hero:not(.started) .hero-controls .h-btn { display: none; }
/* The live frame keeps a speed button at opacity 0 on the left. It is a
   spacer: two icons sit to the right of the red button and only one to the
   left, so without it the red button would not be centred. Kept as the frame
   has it rather than replaced with a margin. */
.h-btn.spacer { visibility: hidden; pointer-events: none; }
/* Same treatment as the bar card's button, different arithmetic because this
   one is CENTRED rather than offset from the left: it needs clearance on both
   sides, so `100cqw - 8px` leaves 4 either end. The min() means nothing changes
   until the card is under 72 wide. At 50 the button is 42. */
.hero-play {
  --play: min(64px, 100cqw - 8px);
  position: relative;
  appearance: none; border: 0; cursor: pointer; background: var(--ihr-red-600);
  border-radius: 999px; display: flex; flex: 0 0 auto;
  width: var(--play); height: var(--play);
  padding: calc(var(--play) * .0625); box-sizing: border-box;
  filter: drop-shadow(0 3px 2.5px rgba(0,0,0,.12));
}
.hero-play img { width: calc(var(--play) * .875); height: calc(var(--play) * .875); display: block; }
/* kind `primary`, color `red`: red550 at rest, red400 for hover and pressed.
   The frames drew this fill as red600, so the resting colour shifts by one step
   here. That is the port doing its job rather than a slip: red550 is the value
   the component uses, and red600 is not a fill the button variant offers. */
.hero-play { background: var(--ihr-red-550); color: var(--true-white);
             box-shadow: var(--elevation1); }
@media (pointer: fine) {
  .hero-play:hover:not([disabled]) { background-color: var(--ihr-red-400); }
}
.hero-play:active { background-color: var(--ihr-red-400); }

/* The bottom block. Podcast gains a scrubber above the waveform while playing,
   in a column with the frame's 4px gap; live radio has no duration and so
   never gets one. */
.hero-bottom { position: relative; display: flex; flex-direction: column; gap: 4px; width: 100%; }
/* The scrubber follows the controls. Hiding it on pause would take away the
   one thing a paused player most needs to show, which is where it stopped. */
.widget.hero:not(.started) .slider { display: none; }
.widget.hero .slider { padding: 0 16px; box-sizing: border-box; }
/* The hero card used to carry its own smaller scrubber geometry, and its own
   track colour, because the old track was a padded box whose padding had to be
   tuned per design. The accomplice Slider separates the hit area from the line,
   so one rule now serves all three and the hero override is gone: the 16px
   min-height it wanted is the component's own default. */
.stage .wave { position: relative; height: 12px; }
.widget.c .stage .wave { height: 24px; }
/* The hero list sits on the card, not on the page, so it keeps square top
   corners against the stage above it. */
.widget.hero .list { border-radius: 0; }

/* ---------- design C, frames 2533:85625 and 2533:91574 ----------
   Design B's card with its controls always on, a list button over the scrubber
   on the podcast side, and a live radio side that is genuinely its own layout
   rather than the podcast minus parts. */
/* Design C's stage has NO aspect ratio.

   It was `aspect-ratio: 16 / 9` clamped between 234 and 360, so width drove
   height and the card only ever held that ratio between 416 and 640 wide. That
   is a video player's contract, not an embed slot's: a slot hands a card a
   width and a height independently, and a card that derives one from the other
   can only honour one of them.

   The Design D frames are not 16:9 either, which is what settled it. Every
   350px frame in that section is 350 by 263; 16:9 at 350 is 197.

   So 263 is the height when the page sets none, taken from those frames, and
   the height control overrides it outright. min-height stays and is not an
   aspect ratio rule: it is the floor protecting the centred control row from
   the 48px top bar and the 40px bottom bar, whose true collision point is 168.
   A slot shorter than that clips rather than overlapping its own controls. */
/* The height iHeart publishes for each type, the same numbers EMBED_H carries
   in the JS, so the card defaults to the slot a real host would give it rather
   than to the 263 its Figma frame happens to be drawn at. 263 was a design
   artifact; these are what the product asserts.

   No min-height here any more. It was 234, the old collision floor, and an
   explicit per type height makes it redundant in auto mode while also being
   larger than the 200 two of the types now ask for. The floor that matters is
   the 136 in the compression block, which applies when a height is set. */
.widget.c[data-kind="podcast"] .stage,
.widget.c[data-kind="artist"] .stage,
.widget.c[data-kind="playlist"] .stage { height: 300px; }
.widget.c[data-kind="episode"] .stage,
.widget.c[data-kind="live"] .stage { height: 200px; }

/* Live radio's play button used to be capped at 60 at its 200 default, because
   its metadata block ran to THREE lines, station then track then artist, which
   made its top bar 52 where the episode card's two lines made 48, and a 64px
   button overlapped the artist line.

   The cap is gone with the third line: track and artist now share one line, so
   the block is two lines like every other card's. The gain is larger than the
   4px the line heights suggest, and that is the interesting part. At three
   lines the text was TALLER than the 48px thumbnail, so the thumbnail's own
   compression did nothing and the top bar stayed 52 at every height. At two
   lines the thumbnail governs again, so the top bar shrinks with the card:
   measured 48 at 300 down to 40 at 150, and the clearance above the button went
   from -7 to +5 there. */
/* The half pixel this carried is gone.

   It arrived with the 16:9 card, whose comment described it rather than argued
   for it, and the ratio it belonged to was removed long ago. What it cost was
   exactly 1px against frame 2700:561776: the play button centred at 49 in a 160
   player where the frame centres it at 48, which left 5 between the button and
   the scrubber against the frame's 6.

   Dropping it also puts the offset on the better half of the cases. The nudge
   made even player heights land on a half pixel and odd ones land whole; plain
   centring is the other way round, and a publisher slot is almost always an
   even number. */
.widget.c .hero-controls { top: 50%; }

/* ---------------------------------------------------------------------------
   Design C given a height.

   A publisher slot hands a card a fixed width AND a fixed height, and until now
   every card here reported the height it wanted and the page obliged. The
   shipping embed does not do this either: measured at 411 wide it draws one
   layout 520 tall and lets the frame clip whatever does not fit, which is why
   the shipped homepage slot at 150 renders its whole episode list below the
   fold where nobody can see it. The first episode row starts at y=201.

   The flag is an attribute rather than the presence of --player-h, because
   container-type cannot be made conditional on a custom property's value.

   container-type: size, not inline-size. It queries BOTH axes, so every width
   query already written here keeps working untouched; the only reason it needs
   a definite height is that a size container may not take its height from its
   contents, which is exactly what a fixed slot is.

   The stage's own height is dropped at EVERY height, not only above the
   threshold, so the slot decides and the card fills it. This used to be about
   clearing an aspect ratio; the ratio is gone now and only the default 263
   remains to clear.

   min-height: 234 stays. It is the floor protecting the centred control row
   from the 48px top bar and the 40px bottom bar; the true collision point is
   168 and 234 carries 66px of deliberate slack. A shorter slot clips, which is
   what the shipping widget already does at every height, now with an honest
   minimum rather than at any size at all. */
.shell[data-fill] {
  /* 100% is the fallback, not decoration. The prototype page sets --player-h
     and embed.html does not: it simply fills the frame it was given. Without
     the fallback the height was invalid there, and a size container takes no
     height from its contents, so the shell collapsed to 0 and the card vanished
     inside an apparently empty slot. */
  container-type: size; height: var(--player-h, 100%); overflow: hidden;
}
/* The shipping embeds answer the height control as well as the width one.

   A slot hands every player on the page the same box, so a comparison run at
   one height and one width is the only honest one; leaving the shipping frames
   pinned at 200 and 440 meant the control changed one side of the comparison
   and not the other.

   This is where the difference shows. Measured at 411 wide, the shipping embed
   draws one layout 520 tall at every height and lets the frame clip it, so
   shrinking the slot crops it and growing the slot leaves grey under it. Design
   C fills whatever it is given. That contrast is the point of the control. */
main[data-h="on"] section[data-design="ship"] .embed iframe { height: var(--player-h); }
/* The chain has to be unbroken. makeWidget renders into a plain div between
   the shell and the card, so height:100% on .widget alone resolved against an
   auto height parent and did nothing at all. */
/* The chain has to be unbroken AND the two links are different properties.

   makeWidget renders into a plain div between the shell and the card, so
   height: 100% on the card alone resolved against an auto height parent and did
   nothing. That div takes height, not min-height: a percentage min-height
   resolves against the parent's SPECIFIED height, and with the div left at auto
   the card's own 100% resolved to nothing, which is why the card sat at 234
   while the div correctly reported 600.

   The card then takes min-height, not height. height: 100% caps it AT the slot,
   so a 150 slot produced a 150 card and the 234 floor could never win. The card
   has to be able to exceed its slot and be clipped, which is the entire point
   of having a floor. Above the floor the two are identical, since the stage
   flexes to fill whatever is left. */
.shell[data-fill] > div { height: 100%; }
.shell[data-fill] .widget.c { min-height: 100%; }
/* NOT a size container, though it should morally be one.

   Everything inside the stage wants to measure the player area rather than the
   whole card, and `container-type: size` here is the obvious way to say that.
   It does not work: the stage takes its height from the flex algorithm, and with
   size containment the container query evaluates it as though it were empty, so
   at a measured 164px stage every max-height query for 127 and 75 matched at
   once and the card collapsed into its compact row at full size.

   So the rules inside keep measuring the CARD and subtract --list-h explicitly,
   which is what --play, --ctl and --thumb all do. */
.shell[data-fill] .widget.c .stage {
  height: auto; flex: 1 1 auto;
}

/* ---------------------------------------------------------------------------
   Design C compressing into a short slot.

   Keyed on the SHELL rather than on the prototype page's <main data-h>. It was
   the page, which meant the compression existed only where the height slider
   did: inside embed.html, which is what host-home.html actually frames, the
   card never learned its height and a 150 slot clipped it instead of
   compressing it. Both pages now mark the shell they have sized, so the same
   rules apply wherever a card is given a box.

   The card had one size for its chrome and a 234 floor, so below that it simply
   clipped. Everything here is driven off cqh, the container's own height, which
   is available because the shell is a size container the moment a height is
   set. None of it applies in auto mode, where the card is 263 and comfortable,
   and every clamp lands on today's value at that height anyway.

   The paddings run 16 down to 8, and the bottom runs 8 down to 0. 6cqh is 15.8
   at 263, so the top of the range is today's 16 and the card only starts
   tightening once the slot is genuinely short.

   The play button takes what is left, and the arithmetic is not the obvious
   one. It is centred on the CARD, while the space it has to fit in is not
   centred on the card: the top bar is 48 and the bottom bar is 32, so the top
   is always the binding side. Half the button has to clear the padding plus the
   48, which gives play = H - 2*padTop - 96, and with padTop at 6cqh that is
   0.88H - 96, the 88cqh below.

   Sizing it against the free band instead, H - 96 - 32, looked right and was
   wrong: it overlapped the top bar by 9px at 160 and by more further down,
   because it spent space it did not have on the short side. Caught by measuring
   the button's top edge against the top bar's bottom edge rather than by eye.

   It holds 64 until that sum says otherwise, which is around 176, and stops at
   40. The width guard stays, since a narrow card can run out of room across
   before it runs out down. */
.shell[data-fill] .widget.c {
  /* round(), so every derived size lands on a whole pixel. The clamps produce
     fractions otherwise, 15.766px of padding at a 263 slot, and a card whose
     rows all start on a subpixel boundary renders soft: straight edges get
     anti-aliased along one axis and not the other, which reads as blur and, on
     a square tile, as not-quite-square. */
  /* --card-pad is no longer interpolated. It used to run 8 at a short card up
     to 16 at a tall one, which meant the same card had two different insets
     depending on the slot it was dropped into. It is 8 at every size now, set
     by the fallback on each rule that reads it, so there is nothing to declare
     here. The bottom is still its own value and still closes up under
     compression, which is where the room actually runs out. */
  --card-padb: round(clamp(0px, 1.5cqh, 4px), 1px);
}
/* Every part of the chrome interpolates between the two frames that exist:
   2669:137077's 350x263 default and 2666:125692's 350x147 compact. Each clamp
   is a straight line through those two anchors, so at 263 the card is exactly
   the tall frame and at 147 exactly the short one, and between them it slides.

     thumbnail    48 -> 40
     play button  64 -> 40
     back and fwd 40 -> 24 glyph, so 48 -> 32 with their padding

   Shrinking the TOP BAR is what actually buys the height, and it is not
   obvious. The play button is centred on the card, so it has to clear the top
   bar on the short side, and the limit is H - 2*padTop - 2*topBar. Dropping the
   thumbnail from 48 to 40 takes 16 off that, which is worth more than the 16
   the bottom row gave back, because the bottom was never the binding side. */
.shell[data-fill] .widget.c .hero-play {
  /* 64 until it would CLIP, rather than shrinking on a ratio from the start.
     It used to interpolate straight between the two frames, 64 at 263 down to
     40 at 147, which meant a 200 slot got a 51px button while 48px of room sat
     unused below it.

     Now it is the smaller of the two things that can actually cut it. The
     button is centred on the card, so each side must clear its own bar and
     each bound is twice the room on that side:
       top     H - 2*padTop - 2*topBar, and the top bar is now the 36 tile
       bottom  H - 2*(bottomBlock + padBottom)
     padTop is a flat 8 now, so the top bound is H - 16 - 72, and it carries 4
     more than that so the button keeps 2px of air rather than sitting flush
     against the bar. It used to read 88cqh - 76 because the padding was itself
     a clamp on height. The podcast card's bottom block is a 16 scrubber, a 4
     gap and a 24 lockup row, so 100cqh - 96.

     Both bounds are now H minus a constant, so which one binds no longer
     depends on the height: on a card with a scrubber the bottom is always the
     tighter of the two by 4, and it is the only one written here.

     H is the STAGE, which is why --list-h comes off first. cqh measures the
     CARD, and the button sits in the stage; with an inline list taking a slice
     the two are not the same. At the shipped 300 threshold the stage never got
     short enough for that to show. */
  --play: round(max(40px, min(64px, 100cqw - 8px, 100cqh - var(--list-h, 0px) - 96px)), 1px);
}
/* No scrubber on these three, so their bottom block is the 24 lockup row alone
   and that bound is 40 looser, which puts the top bar back in charge. Using the
   podcast card's number for all of them shrank the live button while a third of
   the card sat empty under it. */
.shell[data-fill] .widget.c[data-kind="live"] .hero-play,
.shell[data-fill] .widget.c[data-kind="artist"] .hero-play,
.shell[data-fill] .widget.c[data-kind="playlist"] .hero-play {
  --play: round(max(40px, min(64px, 100cqw - 8px, 100cqh - var(--list-h, 0px) - 92px)), 1px);
}
/* The transport holds 40 the way the play button holds 64, instead of
   interpolating from the top of the range. These were a straight line on height,
   13.8cqh + 3.7, so a 200 slot got 31px icons with a hundred pixels of unused
   room around them: they shrank while nothing was pressing on them, which is the
   same complaint the play button had.

   They take the play button's own bound rather than a bound of their own,
   because they share its row. The row is as tall as its tallest child and has to
   clear the same top bar and bottom block, and the button is always the taller
   of the two, so whatever fits it fits these. Floor 24, the compact frame's own
   size, against the button's floor of 40.

   The width term is theirs alone, because unlike the button they sit in a row
   BESIDE something. The centre column is the button and each side column is half
   of what is left, so the wider side has to hold two buttons and a gap:
   2*(ctl + 8) + 8 <= (100cqw - 16 - play)/2, which with the button at its 64
   maximum is ctl <= 25cqw - 32. Without it, holding 40 pushed 1x off the left
   edge of a 280 card, where the old interpolated size still fitted. */
.shell[data-fill] .widget.c {
  --ctl: round(max(24px, min(40px, 100cqh - var(--list-h, 0px) - 96px, 25cqw - 32px)), 1px);
  /* The tile, sized from the CARD rather than from the text beside it.

     It was 36 at every height and every width, because fitThumb() measured the
     text block and the text block is always two lines. That is 12 under the
     frame: 2676:542137 draws it at 48 square with a 6px radius.

     Two anchors are the design's own and two are chosen. 9.5cqh + 23 passes
     through 48 at 263, which is the frame this card is drawn at, and reaches 80
     at 600. Below that it falls to the 36 floor at around 137, and above 600 it
     holds 80. The gentler slope of the 600 anchor also keeps the tile nearer the
     frames at short heights: 42 at a 200 card, against the 40 the compact frame
     draws, where the steeper 440 version had already dropped to the floor.

     The width term is not decoration. The row also carries the title, a gap and
     two action buttons, so an 80px tile on a 280px card would leave the title
     almost nothing; this caps the tile by what is actually left over.

     The rule itself lives with the list thresholds further down, because it has
     to subtract whatever the inline list is taking. It was duplicated here for a
     while, and this copy was dead: the later one won on source order. */
}
/* No scrubber on these three, so the same 40 looser bound the button gets. */
.shell[data-fill] .widget.c[data-kind="live"],
.shell[data-fill] .widget.c[data-kind="artist"],
.shell[data-fill] .widget.c[data-kind="playlist"] {
  --ctl: round(max(24px, min(40px, 100cqh - var(--list-h, 0px) - 92px, 25cqw - 32px)), 1px);
}
.shell[data-fill] .hero-controls .h-btn[data-act="back"] img,
.shell[data-fill] .hero-controls .h-btn[data-act="fwd"] img,
.shell[data-fill] .hero-controls .h-btn[data-act="next"] img {
  width: var(--ctl); height: var(--ctl);
}
/* 1x is a text box rather than a glyph, so it needs the same clamp written for
   it or it does not compress. It matched its neighbours at 300 and stood 48
   beside their 42 at 220 and their 34 at 160, which is exactly the mismatch
   this rule removes. The type steps with the box on production's own rule,
   caption-3 at 12 up to caption-1 at 14 when there is room. */
.shell[data-fill] .hero-controls .h-speed {
  width: var(--ctl); height: var(--ctl); line-height: var(--ctl);
  /* Tied to the box rather than to the card, so the type steps down when 1x
     actually gets smaller and not before. Production's own range, caption-3 at
     12 up to caption-1 at 14. */
  font-size: round(clamp(12px, var(--ctl) * 0.35, 14px), 1px);
}
/* No floor, and a different layout rather than less of the same one.

   There used to be a floor, most recently 136, and under it the card kept its
   full height inside a shorter slot and let the slot clip the overflow. That
   cost the one thing the card cannot afford to lose: the play button is centred
   on the card, so a 50px slot showing the top 50px of a 136px card showed the
   top bar and the top third of the button.

   Hiding the top bar under 128 fixed the clipping and cost the metadata, which
   is the wrong trade. The button is centred on the card, so it competes with
   the top bar for the same vertical space; measured, they meet at 128. Putting
   the button INTO that row removes the competition instead of resolving it, and
   the whole line is then 40 tall including the 36 tile, which fits any card from
   56 up. So the metadata survives all the way down instead of vanishing at 128.

   Three steps, each threshold measured rather than derived:
     under 128  the stacked hero becomes one row, play at the left
     under 76   the bottom block drops, where it stops fitting under that row
     under 56   the inset gives way, the last place a 40px button can be kept
   The button never shrinks past 40 and is never cut, at any height down to 50. */
@container (max-height: 127px) {
  /* The button leaves the centre of the card and takes the left of the top bar.
     .hero-controls is already absolute against the stage, so this is a move
     rather than a reparent, and the row makes space for it with padding. */
  /* Both selectors, and both carrying the shell. The live card sets its own
     .widget.c.live .hero-controls top further down the file to nudge the button
     half a pixel, which is more specific than a plain .widget.c and later in the
     file besides, so a single rule here moved every card except the one that
     needed it most. */
  .shell[data-fill] .widget.c .hero-controls,
  .shell[data-fill] .widget.c.live .hero-controls {
    left: var(--card-pad, 8px); top: var(--card-pad, 8px);
    transform: none; width: auto; padding: 0; justify-content: flex-start;
  }
  /* Seek, speed and skip go. There is no room for them beside the button and
     they are not what a card this short is for. */
  .widget.c .hero-controls .cc-side { display: none; }
  /* The trailing buttons used to go outright here, on the reasoning that at this
     height the row is the whole card and the title needs the width more than
     three secondary actions do. The breakpoints matrix keeps them, just later:
     share from 241, list from 301, info from 341, against 201, 241 and 341 in
     the taller rows. That staircase is written once further down and carries the
     height condition with it, so this block no longer hides them at all. */
  .widget.c .topbar { padding-left: calc(var(--card-pad, 8px) + 48px);
                      min-height: 40px; }
  /* The stage is a flex column with space-between, and with the row pinned to
     the top the bottom block is effectively its only free child, which
     space-between puts at the START. margin-top:auto pins it down regardless of
     how the column is justified. */
  .widget.c .hero-bottom { margin-top: auto; }
}

/* The whole bottom bar goes at 100 tall and below, from the breakpoints matrix
   in Figma, section 2719:602052. Neither the scrubber nor the brand lockup is
   drawn anywhere in its 50-100 row, at any width.

   This was 75, measured rather than derived: at 76 the bottom block still
   cleared the compact row by 3px and at 70 it was already past the bottom of the
   card. So 76 was the last height at which it physically fitted. Figma asks for
   it to go 25px earlier than that, which is a judgement that a bar which only
   just fits is not worth the room, not a correction to the measurement. The old
   number is kept here because it is the one that says where the hard limit is,
   if the 100 is ever revisited. */
@container (max-height: 100px) {
  .widget.c .hero-bottom { display: none; }
}

/* The inset is 8 everywhere except here. A 40px button plus 8 above and 8 below
   needs 56, so below 56 the padding is the only variable left that can keep the
   button whole and at its stated 40 minimum. It gives exactly as much as it has
   to and no more: 8 at 56, 5 at 50. */
@container (max-height: 55px) {
  .widget.c { --card-pad: round(max(0px, 50cqh - 20px), 1px); }
}

/* The list's default-hidden rule is gone: display is driven by --list-display
   now, whose fallback IS none, so the default lives in one place instead of
   depending on which of two equally specific rules came last in the file. */

/* 300, down from 440.

   440 was 220 stage + 220 list, two equal halves. Below that they cannot both
   hold: a fixed 220 list at 300 leaves the stage 80, well under the 136 its
   chrome needs, so lowering the threshold only works if the LIST shrinks too.

   It does, between the two ends. 220 at 440 down to 128 at 300, which is the
   least a list can be and still read as one: a 32 header, one 72 row, and the
   24 that says there is more below. The stage takes what is left, 220 at 440
   and 172 at 300, comfortably clear of its 136 floor.

   Above 440 nothing changes. The list caps at 220 and every further pixel goes
   to the artwork, exactly as before.

   min-height is removed rather than lowered. At exactly 440 the stage is
   440 - 220 = 220, which is BELOW the 234 floor, and left alone the two rules
   fight and the card overflows its slot by 14px at precisely the threshold.
   Above the threshold the stage is height - 220 by definition, so it can never
   be under 220 and no floor is needed. 220 is still 52px clear of the 168
   collision point.

   The drawer is taken out entirely up here rather than left underneath. Its
   button is gone, so a drawer left open by dragging the slider upward would
   have been sitting over the artwork with the inline list showing below it. Its
   open state survives, so dragging back under 440 returns it as it was.

   .h-btn in that last selector is load-bearing. The drawer's own close button
   also carries data-act="list", because closing the drawer is the same toggle
   as opening it; a selector written as [data-act="list"] alone would hide the
   close button too. It is .sheet-close, not .h-btn, which is what keeps the
   two apart. */
/* WHERE the inline list appears, and what it does when it gets there.

   300 is the threshold. 170 and 240 were here too for a while, as lower ones to
   compare it against, and they are gone: at either of them the player is left so
   short that the compact row has to take it over, so what they were really being
   compared on was a different card rather than a different threshold. What is
   left is 300 and Reveal, and each block below only SETS custom properties, with
   the rules those properties drive living once, further down. Changing the
   behaviour means editing one rule, not two.

   Whole rows, plus a constant sliver of the next one. The list height used to
   glide continuously while a row is a fixed 72, so how much of the next row
   showed was an accident of where the clamp landed: 8px at 300, 47px at 360, 2px
   at 400. round(down, ..., 72px) snaps the row AREA to whole rows and adds back
   whatever is not rows.

   That was a hardcoded 64 and is --list-fix now, because the episode list has
   no heading any more and the two lists no longer carry the same overhead. 64
   is 12 of top padding, a 32 header, the 4 gap under it and the 16px peek; the
   episode list's 28 is the same sum with the header and its gap taken out. It
   is written once on .list, so a list that gains or loses furniture changes one
   number rather than three. Stepping follows: the episode list needs 36 less
   for the same rows, so it reaches two rows at a 367 card where the others
   still wait until 422.

   The guard is on :root, not on <main>. embed.html and host-home.html have no
   <main> at all, so a guard hung off one matched nothing there and an embedded
   card lost its inline list entirely.

   The 300 block is written as "not reveal and not figma" rather than as "is
   300", so it also covers the attribute being absent. embed.html and
   host-home.html run no script to set it, and guarding it on the value alone
   left them matching no mode at all and showing no inline list at any height. */
@container (min-height: 300px) {
  :root:not([data-listat="reveal"]):not([data-listat="figma"]):not([data-list="drawer"]) .widget.c {
    --list-display: flex; --listbtn-display: none; --listsheet-display: none;
    --stage-min: 0;
  }
  /* --list-h only on the kinds that actually carry a list. Unscoped it docked
     the list's height off the episode and live cards too, which have none, and
     gave them a tile sized for a player area smaller than the one they have. */
  :root:not([data-listat="reveal"]):not([data-listat="figma"]):not([data-list="drawer"]) .widget.c[data-kind="podcast"],
  :root:not([data-listat="reveal"]):not([data-listat="figma"]):not([data-list="drawer"]) .widget.c[data-kind="artist"],
  :root:not([data-listat="reveal"]):not([data-listat="figma"]):not([data-list="drawer"]) .widget.c[data-kind="playlist"] {
    --list-h: calc(var(--list-fix, 64px) +
                   round(down, clamp(72px, 65.7cqh - 69.1px - var(--list-fix, 64px), 144px), 72px));
  }
}

/* "Reveal", the shipping player's shape rather than ours.

   Ours decides a list height from the card and lets the stage take the rest, so
   the list arrives whole at its threshold. The shipping embed has no height
   logic at all: it draws one tall layout and the slot clips it, so as the slot
   grows the list is uncovered a row at a time and the player never moves.

   This reproduces that deliberately. The player holds 160 and the list takes
   everything above it, up to the same 208 maximum the stepped modes use; past
   that the list is full and the player grows instead. So:

     card <= 172   no list, all player
     173 to 368    player fixed at 160, list revealing 13 -> 208
     above 368     list full at 208, player takes the rest

   173 rather than 161, because .list carries 12px of top padding and every box
   here is border-box, so the list cannot render shorter than that padding. From
   161 to 172 it therefore sat at 12 while the clamp asked for less, and the
   player was squeezed to 149 instead of holding its 160. Starting the reveal
   where the list first clears its own padding keeps the player's hold exact.

   No round(down, ...) here, which is the point: the reveal is partial rows by
   design, where the stepped modes snap to whole ones and keep a constant peek. */
@container (min-height: 173px) {
  :root[data-listat="reveal"]:not([data-list="drawer"]) .widget.c {
    --list-display: flex; --listbtn-display: none; --listsheet-display: none;
    --stage-min: 0;
  }
  :root[data-listat="reveal"]:not([data-list="drawer"]) .widget.c[data-kind="podcast"],
  :root[data-listat="reveal"]:not([data-list="drawer"]) .widget.c[data-kind="artist"],
  :root[data-listat="reveal"]:not([data-list="drawer"]) .widget.c[data-kind="playlist"] {
    --list-h: clamp(0px, 100cqh - 160px, 208px);
  }
}

/* "Figma", frame 2700:561775, widget_160+, 420 by 300.

   The other three modes are ours. This one is the design file's own answer, and
   unlike the older frames it is not a drawing at one size: the frame carries
   real auto-layout constraints, so resolving them IS the responsive spec.

     player_container   flex 1 0 0, min-height 160, padding 8
     List               flex 1 0 0, max-height 208, min-height 1
     top_bar            flex 1 0 0, max-height 80, min-height 36
     middle_bar         shrink 0, a 64 play button with 2 above and below, 68
     bottom_bar         flex 1 0 0, max-height 64
     art                aspect 1/1, height 100% of top_bar

   Two children of the card, both flex 1 0 0, so every pixel the card gains is
   split evenly between them until one hits a bound. That is the whole model,
   and it resolves in three bands:

     161 to 319    an even split would leave the player under its 160 floor, so
                   the player freezes at 160 and the list takes the remainder
     320 to 416    both are within bounds, so they grow together, half each
     above 416     the list is frozen at its 208 maximum and the player takes
                   the remainder

   The arithmetic is the frame's own: at 300 it gives 160 and 140, which is the
   420 by 300 geometry as drawn, and the player's parts sum to 8 + 38 + 68 + 38
   + 8 = 160 exactly.

   min(100cqh - 160px, 50cqh, 208px) is those three bands written once. The
   first term is the floor band, the second the even split, the third the cap,
   and the smallest is always the one in force. Checked against the frame at
   200 -> 40, 320 -> 160, 368 -> 184, 440 -> 208.

   Where this differs from Reveal, which is the mode it is closest to: Reveal
   pours every new pixel into the list until it fills and only then grows the
   player, so between 321 and 415 it runs ahead of the design by up to 24px at
   368, where the frame wants 184 and 184 and Reveal renders 160 and 208. */
@container (min-height: 161px) {
  :root[data-listat="figma"]:not([data-list="drawer"]) .widget.c {
    --list-display: flex; --listbtn-display: none; --listsheet-display: none;
    --stage-min: 0;
  }
  :root[data-listat="figma"]:not([data-list="drawer"]) .widget.c[data-kind="podcast"],
  :root[data-listat="figma"]:not([data-list="drawer"]) .widget.c[data-kind="artist"],
  :root[data-listat="figma"]:not([data-list="drawer"]) .widget.c[data-kind="playlist"] {
    --list-h: min(100cqh - 160px, 50cqh, 208px);
  }

  /* The frame's List is min-height 1 and clips its own padding, so it can be a
     single pixel tall. A border-box element cannot: 12px of top padding makes
     12 its shortest possible render, which is why Reveal starts at 173 rather
     than 161. Collapsing the padding alongside the sliver is what lets this
     mode start where the frame does. It is back to a full 12 by card 172, so
     nothing above that band is touched. */
  :root[data-listat="figma"]:not([data-list="drawer"]) .widget.c .list {
    padding-top: min(12px, 100cqh - 160px);
  }

  /* The artwork, which is where the frame differs from us most.

     Ours sizes the tile from the CARD with a clamp whose upper anchor we chose
     rather than took from a frame, so it only reaches 80 at a 900 card. The
     frame ties the tile to top_bar, which is flex 1 0 0 inside the player and
     capped at 80, so the tile is (player - 8 - 68 - 8) / 2 and locks at 80 once
     the player reaches 244.

     Substituting each band's player height gives the three terms below.
     Constant 38 while the player is frozen at 160, 25cqh - 42 while the player
     is 50cqh, and 50cqh - 146 once the player is 100cqh - 208. The largest is
     always the one in force, and they meet exactly at the band edges: 38 at
     320, 62 at 416, 80 at 452.

     The width term is not the frame's. At 420, the width the frame is drawn at,
     it never binds. It is kept because the frame has nothing to say about
     narrow cards and without it an 80px tile on a 280px card leaves the title
     almost nothing. Every height behaviour below 420 wide is the frame's.

     Scoped to the three kinds that carry a list, because the three terms are
     each a band of the WITH-LIST player height. The episode and live cards have
     no list, so their player is the whole card and none of the three describes
     them. The frame has nothing to say about those two, so they keep the rule
     every other mode gives them and the toggle stays a clean comparison on the
     cards the frame is actually about. */
  :root[data-listat="figma"]:not([data-list="drawer"]) .shell[data-fill] .widget.c[data-kind="podcast"],
  :root[data-listat="figma"]:not([data-list="drawer"]) .shell[data-fill] .widget.c[data-kind="artist"],
  :root[data-listat="figma"]:not([data-list="drawer"]) .shell[data-fill] .widget.c[data-kind="playlist"] {
    --thumb: round(max(36px, min(80px,
                                 max(38px, 25cqh - 42px, 50cqh - 146px),
                                 100cqw - 216px)), 1px);
  }
}

/* What those properties drive, written once.

   Each falls back to the pre-threshold state, so a card below its threshold, or
   a page with no switch at all before the script runs, reads as it always did:
   no inline list, a list button, a working drawer. */
.widget.c .list { display: var(--list-display, none); flex: 0 0 var(--list-h); }
.widget.c .stage { min-height: var(--stage-min, auto); }
.widget.c .h-btn[data-act="list"] { display: var(--listbtn-display, flex); }
.widget.c .sheet:not(.info-sheet) { display: var(--listsheet-display, flex); }

/* The tile is sized from the STAGE once a list is on the card, not from the
   card. cqh is the card, and the card is not what the tile sits in: the list
   takes a slice of it, so the card height overstates the room the tile has.

   The effect was backwards rather than merely inexact. At their defaults the
   episode card's player area is LARGER than the podcast card's, 200 against 172,
   because the podcast card spends part of its 300 on the list, and yet the
   podcast tile came out bigger. Subtracting the list puts both on one rule: same
   player area, same tile.

   var(--list-h, 0px) rather than three per-threshold copies: a card with no
   inline list simply has no --list-h, subtracts nothing, and takes the base
   formula. That also retires the per-kind scoping this rule used to need. */
.shell[data-fill] .widget.c {
  --thumb: round(max(36px, min(80px, 9.5cqh + 23px - 0.095 * var(--list-h, 0px),
                               100cqw - 216px)), 1px);
}
/* Live C ends in a waveform running to the edge, like every other card here,
   so it takes no bottom padding. Its own frame parked three icons at the bottom
   right instead and had no waveform at all; the control row and the waveform
   come from 2512:111876, which is where that row is drawn. The rest of the card
   stays design C's own, the 48px tile and the single line of text. */
.widget.c.live .hero-controls { top: calc(50% - .5px); }
/* 48 everywhere now, so the live-only rule is gone. */
/* The list button sits 8px above the scrubber, and the pair sits 4px above the
   waveform, which is two nested columns in the frame and two here. */
/* 4, not 8. Frame 2666:125692 closes this up along with moving the list button
   out of the row below, and the two together are most of what makes the card
   fit a shorter slot. */
.c-rows { display: flex; flex-direction: column; gap: 4px; width: 100%; }

/* The Listen on iHeart lockup, frame 2613:77793. It replaces the badge that
   used to sit in design C's top right, and the share button took that corner.
   12/16 copy, an 8px gap, then the logotype at 14 tall with its width following
   the 107 by 24 box. Measured against the frame: 49 + 8 + 62, ending 16 in from
   the card's right edge. */
/* 3, not 8. The mark reads as the last word of the sentence, so the space
   before it is the space between the words: "Listen on" at 12px SemiBold Open
   Sans measures a 3.12px word space, and 3 is that rounded. */
.ihr-lockup { display: inline-flex; align-items: center; gap: 3px;
              text-decoration: none; flex: 0 0 auto; }
.ihr-lockup span { font-size: var(--fs-12); line-height: var(--lh-16); font-weight: 600;
                   letter-spacing: var(--ls-4); color: var(--true-white); white-space: nowrap; }
.ihr-lockup img { display: block; height: 14px; width: auto; }
.ihr-lockup:hover { opacity: .85; }
.ihr-lockup:focus-visible { outline: 2px solid var(--true-white); outline-offset: 2px;
                            border-radius: 4px; }
/* Under 240 the row cannot hold both. The left group is 68 and the full lockup
   123, which with 16 of padding each side wants 223 before there is any gap at
   all; measured, the two touch at 200 and the lockup then runs 11 past the card.
   The words go and the logotype stays, because the mark is the attribution and
   "Listen on" is the sentence around it. That fits down to about 170. */
/* The lockup keeps its words at every width. They used to be hidden under 239 to
   buy room on the row, but the attribution is the point of the embed on someone
   else's page and "Listen on" is half of it. The logotype alone does not say
   what the button does. */

/* Design C's share sits in the top bar now, as a real flex child at the end of
   the row rather than absolutely placed the way the badge was, so the metadata
   shortens to make room for it instead of running underneath. */
.tb-share { flex: 0 0 auto; }

/* Its tooltip is the only one on any card that has to open DOWNWARD.

   Every other one opens above its button, which is right for controls sitting
   in the middle or at the foot of the card. This button is 24px from the card's
   top edge inside a stage that clips its overflow, so a bubble above it landed
   at -8 and was cut in half. Measured before the fix: `top: -32px` against a
   button at y 24.

   So the bubble and the beak both flip to the underside, the beak's path is
   drawn pointing up, and the entry offset is negated so it still slides IN
   towards the button rather than away from it. The 4px overlap that hides the
   shadow across the join works out the same either way: the beak runs from +4
   to +12 and the bubble starts at +8.

   It also anchors to the right edge, for the reason the bottom row's do: a
   bubble centred on a button 16 in from the card edge overhangs at every
   width. */
/* EVERY button in this row, not only share. Share was the only one here when
   this was written; info and list have since moved up beside it and kept
   opening upward, which put their bubbles above the card's top edge inside a
   stage that clips, so they were invisible. The arrow path is redrawn pointing
   up for all of them. */
.topbar .h-btn[aria-label]::after {
  bottom: auto; top: calc(100% + 8px);
  transform: translateX(-50%) translateY(calc(var(--sp-8) * -1));
}
.topbar .h-btn[aria-label]::before {
  bottom: auto; top: calc(100% + 4px);
  transform: translateX(-50%) translateY(calc(var(--sp-8) * -1));
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='8'%3E%3Cpath d='M0 8 L8 0 L16 8' fill='%23ffffff' stroke='%23ffffff' stroke-width='1' stroke-linecap='round'/%3E%3C/svg%3E");
}
.topbar .h-btn[aria-label]:hover::after,
.topbar .h-btn[aria-label]:hover::before,
.topbar .h-btn[aria-label]:focus-visible::after,
.topbar .h-btn[aria-label]:focus-visible::before {
  transform: translateX(-50%) translateY(0);
}
/* Share alone hangs from its right edge rather than centring, because it is the
   last button in the row and a centred bubble overhangs the card at every
   width. The others sit inboard and centre normally. */
.topbar .tb-share[aria-label]::after { left: auto; right: 0; transform: translateX(0) translateY(calc(var(--sp-8) * -1)); }
.topbar .tb-share[aria-label]::before { left: auto; right: 8px; transform: translateX(0) translateY(calc(var(--sp-8) * -1)); }
.topbar .tb-share[aria-label]:hover::after,
.topbar .tb-share[aria-label]:hover::before,
.topbar .tb-share[aria-label]:focus-visible::after,
.topbar .tb-share[aria-label]:focus-visible::before { transform: translateX(0) translateY(0); }
/* Two groups, one at each end: speed and list to the left, plus, info and
   share to the right, which is what frame 2533:85625 draws. gap 4 within each
   group, 16 in from each edge. space-between rather than flex-end, so the row
   needs both spans present even when the left one is empty, which is the live
   radio case. */
/* 32 tall whatever is in it.

   The row is sized by its tallest child, and its children differ by content
   type: podcast puts 32px buttons in it, 1x and the list, while live has none
   of those and only the 16 tall lockup. So the row collapsed to 16 on live and
   the "Listen on iHeart" lockup sat 16 above the card's bottom edge against
   podcast's 24. The row's own box was 16 from the edge on both; it was the row's
   HEIGHT that differed, which is the harder version of the bug to see.

   A floor of 32, the height of the buttons that set it on podcast, centres the
   lockup in the same box on both. */
/* 16, not 32 and not the 24 this was. 32 was the height of the buttons that
   used to sit in this row, and 24 was a guess at what the lockup needed once
   they moved to the top bar.

   The frame answers it. brand, 2700:561799, is a plain row of "Listen on" at
   12 on a 16 line beside a 62 by 14 logo, so it is 16 tall and carries no
   minimum of its own. Our own content is already exactly 16; the 24 was padding
   it out by 8.

   Those 8 were visible, and not where they were being added. bottom_bar is
   anchored to the bottom of the player, so a bottom block 8 too tall pushes the
   SCRUBBER up by 8 rather than pushing the brand row down. At the 160 player the
   frame is drawn at, that put the scrubber 3px over the play button instead of 6
   clear of it, a 9px error on the one gap in the player with nothing between its
   two sides. Design C only: the other designs still put controls in this row. */
.widget.c .list-row { min-height: 16px; }

/* "Listen on" is Digital Brand Caption 3 in the frame, 12 / 600 / 16 with
   letterSpacing -0.2. The size, weight and line height were already right and
   the tracking was not set at all. */
.widget.c .ihr-lockup { letter-spacing: var(--ls-0); }

/* ---------- player_container spacing, frame 2700:561776 ----------
   Design C only. The card carries `widget hero c`, so several of these are
   overriding a value the hero card set for its own frames, not a value chosen
   for this one.

   The frame puts a flat 8 of padding on player_container and nothing on its
   three children. Ours pushes the horizontal 8 onto each child instead, which
   lands in the same place, so only the values that actually differ are here.

     bottom_bar gap          2, was 4
     Slider inset            8, was 16, the hero card's own value
     player_container bottom 8, was up to 4
     middle_bar padding      2 top and bottom, was 0
     right_buttons gap       4, was 8

   c-rows is ours, not the frame's. It exists because the live card has no
   scrubber, so it wraps the two rows the frame keeps directly in bottom_bar.
   It therefore carries the frame's bottom_bar gap, and hero-bottom carries it
   too for the cards where c-rows is not there. */
.widget.c .hero-bottom, .widget.c .c-rows { gap: 2px; }

/* The hero card insets its scrubber 16, which put the time labels 8 further in
   than the artwork above them and the lockup below. The frame runs the slider
   the full width of the padded container, so it aligns with both. --card-pad,
   not a flat 8, so it closes up with everything else on a 50px card. */
.widget.c .slider { padding: 0 var(--card-pad, 8px); }

/* The frame's bottom padding is 8, the same as the other three sides. The hero
   card's --card-padb tops out at 4 and closes to 0 under compression, and the
   compression is worth keeping, so this reaches the frame's 8 at exactly the
   160 the frame is specified from and gives the room back below that. 5cqh is
   what puts 8 at 160. */
.shell[data-fill] .widget.c .hero-bottom { padding-bottom: round(clamp(0px, 5cqh, 8px), 1px); }

/* middle_bar is 2 above and below playback_controls, which is what makes the
   frame's middle block 68 rather than 64 and is already the number the Figma
   list mode's artwork rule is derived from. The compact row zeroes this along
   with the rest of the padding when it takes the controls out of the column. */
.widget.c .hero-controls { padding: 2px var(--card-pad, 8px); }

/* The frame's two control groups are not symmetrical. left_buttons is gap 8 and
   right_buttons is gap 4, so the right group is copied here as its own value.

   right_buttons also carries 4 of padding where left_buttons carries none, and
   that one is NOT copied. It is the only value in the frame that would move a
   control off-centre against its opposite number, 12 from the play button on
   the right against 8 on the left, and it reads as a drafting slip rather than
   an intent. Say the word and it goes in. */
.widget.c .hero-controls .cc-side + .hero-play ~ .cc-side,
.widget.c .hero-controls .cc-side:last-child { gap: var(--sp-4); }
.list-row { display: flex; justify-content: space-between; align-items: center;
            gap: var(--sp-4); min-height: 32px;
            padding: 0 var(--card-pad, 8px); box-sizing: border-box; }
.lr-side { display: flex; align-items: center; gap: var(--sp-4); }
/* The drawer, frame 2533:85717. It slides up over the player inside the card
   rather than opening a panel beneath it, which is what the frame draws: the
   Drawer instance is the same 358 by 263 box as the player, at 0,0.
   It is closed by transform rather than by display, so the slide can be seen,
   and visibility flips only at the end of the travel so a closed drawer is out
   of the tab order rather than sitting invisibly over the controls. */
/* accomplice Drawer, bottom placement, for all three drawers on the card.
   These two were full-card sheets taken from the frames while the share drawer
   was built to the component, so one card had drawers behaving two ways: 100%
   of the card at 0.28s, and 80% at 600ms. The component wins, because the whole
   point of porting to it was to stop the prototype drifting from the system.

   The shape is the share drawer's: .sheet is a transparent full-card container
   that pushes its panel to the end, and .sheet-panel is the surface, capped at
   80% with the 6px 6px 0 0 radius and the 600ms slide. The container is what
   positions, rather than the panel being anchored with bottom:0, because on the
   bar card .widget wraps the player AND the episode list, so the bottom of it
   is below the list rather than the bottom of the card you can see. */
.sheet {
  position: absolute; inset: 0; z-index: 3; width: 100%;
  display: flex; flex-direction: column; justify-content: flex-end;
  overflow: hidden; pointer-events: none;
  visibility: hidden; transition: visibility 0s linear .6s;
}
.sheet-panel {
  /* Full height, not the component's 80% cap. 80% reads fine on a full page,
     where a fifth of a tall viewport is still a lot of drawer. These cards are
     180 to 444 tall, so the same fraction left barely enough room to read an
     episode list or a show description in. Height rather than max-height, so a
     drawer with little content still fills the card instead of sitting in a
     short strip. This is the one place the Drawer's own geometry is overruled,
     and the reason is the container it is in. */
  pointer-events: auto; width: 100%; height: 100%; max-height: 100%;
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--true-white);
  border-radius: var(--rad-6) var(--rad-6) 0 0;
  transform: translateY(100%);
  /* Entering and leaving are NOT the same curve in production. accomplice runs
     `slideInBottom 600ms` on the way in, which is the default `ease`, and
     `slideInBottom 600ms reverse ease-in` on the way out. This had `ease` both
     ways, so the exit crawled out of the card the way the entry crawls in. The
     rest state carries the LEAVING curve; the open state overrides it. */
  transition: transform .6s var(--drawer-out);
}
/* :not(.info-sheet), because the info drawer also carries the .sheet class and
   was therefore being opened by the episodes flag as well. It sits on a higher
   layer, so pressing the list button showed the information instead. */
.widget.sheet-open .sheet:not(.info-sheet) { visibility: visible; transition: visibility 0s; }
/* Same promotion as the share panel, and for the same reason: these slide the
   full height of the card carrying rows of artwork and text. */
.sheet-panel { will-change: transform; }
.widget.sheet-open .sheet:not(.info-sheet) .sheet-panel { transform: translateY(0); }
.widget.sheet-open .sheet-panel, .widget.info-open .sheet-panel { transition: transform .6s var(--drawer-in); }
@media (prefers-reduced-motion: reduce) {
  .sheet { transition: visibility 0s; }
  .sheet-panel { transition: none; }
}
.sheet-head { flex: 0 0 auto; display: flex; align-items: center; gap: 16px;
              padding: 16px; background: var(--ihr-grey-200); }
/* accomplice's Drawer heading at its MOBILE size, everywhere, which is what
   this already was: drawer.css.ts sets fontSize 18 with 16 of side padding and
   only steps to 24 with 24 of padding at breakpoints.large. Measured constant
   18 / 24 / 700 with 16px padding from 300 through 1200 wide.

   One value did differ. The letter spacing was 0, taken from Figma's H5 type
   style, where the component itself uses letterSpacing[1], which is -0.05rem
   at accomplice's 10px rem base and so -0.5px here. The component wins, since
   the ask was to follow accomplice and iheart.com. */
.sheet-head h3 { flex: 1 1 auto; min-width: 0; margin: 0;
                 font-size: var(--fs-18); line-height: var(--lh-24);
                 font-weight: 700; letter-spacing: var(--ls-1); color: var(--ihr-grey-600);
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sheet-close { flex: 0 0 auto; appearance: none; border: 0; background: transparent;
               cursor: pointer; padding: 4px; border-radius: 999px; display: flex; }
.sheet-close img { width: 24px; height: 24px; display: block; }
/* A dark wash, not the white one the icon buttons over artwork use, since this
   header is light. */
.sheet-close:hover { background: rgba(0,0,0,.08); }
/* Only the SIDES here. The block padding moves onto the scrolling content
   below, and that is the whole fix for the white band: with 16px of padding on
   the scroll container the content was clipped 16 short of the panel's edge,
   so a part row was cut and then a strip of white sat under it, which reads as
   something covering the list rather than a list that carries on. Inside the
   scroller the same 16 scrolls with the content, so a cut row is cut at the
   panel edge and the space appears only when you reach the end. */
.sheet-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 0 16px; }
.sheet-body > .rows { padding-top: 16px; }
.sheet-body > .info-body { padding-block: 16px; }
/* In the LIST drawer the rows scroll, not the body around them. The body keeps
   the overflow for the info drawer, whose content is a block of text with no
   scroller of its own.

   fitPeek used to force an explicit height on .rows, and that was quietly the
   only thing bounding it. With it gone the rows grew to their full content
   height, the body took over the scrolling, and the drawn scrollbar, which is
   attached to the rows, saw nothing to scroll. */
.sheet-body.rows-body { overflow: hidden; display: flex; flex-direction: column; }
.sheet-body .rows { gap: 2px; }

/* The info drawer, frame 2562:285883. Same component, one layer higher so it
   sits over the episodes drawer when both are asked for. The radius lives on
   the panel now, and is top-only like every other bottom drawer. */
.info-sheet { z-index: 4; }
.widget.info-open .info-sheet { visibility: visible; transition: visibility 0s; }
.widget.info-open .info-sheet .sheet-panel { transform: translateY(0); }
/* The title wraps to two lines here rather than ellipsising, which is what
   takes the header from 64 to 80. */
.info-sheet .sheet-head h3 { white-space: normal; overflow: visible; text-overflow: clip; }
/* body-3, exactly: 16 / 24 / 400 / -0.5. */
.info-body { margin: 0; font-size: var(--fs-16); line-height: var(--lh-24);
             letter-spacing: var(--ls-1); color: #000; }

/* ---------- marquee ----------
   Text that does not fit still ellipsises. On a device with a real pointer it
   also scrolls on hover so the rest can be read. The inner span is plain
   inline until it is hovered, which keeps the ellipsis working normally; an
   inline-block child and text-overflow do not reliably agree. Touch has no
   hover state, so it simply keeps the ellipsis. */
/* accomplice Marquee. The recipe's base is max-width 100%, overflow hidden,
   position relative, text-overflow ellipsis, white-space nowrap, width 100%,
   and it flips from block to flex once it has measured that the text overflows,
   which is the `ready` variant. This page measures the same thing and marks it
   with .over, so the two agree on behaviour; the declarations below are the
   component's base verbatim. */
.mq { max-width: 100%; width: 100%; overflow: hidden; position: relative;
      white-space: nowrap; text-overflow: ellipsis; }
@media (hover: hover) and (pointer: fine) {
  .mq.over:hover { text-overflow: clip; }
  .mq.over:hover .mqi {
    display: inline-block; will-change: transform;
    animation: marquee var(--mq-dur, 6s) linear infinite;
  }
}
@keyframes marquee {
  0%, 12%  { transform: translateX(0); }
  50%, 62% { transform: translateX(var(--mq-shift, 0)); }
  100%     { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  .mq.over:hover .mqi { animation: none; }
}

/* Wide layout. The artwork spans the full height beside BOTH the text and the
   controls, which is what the 1280 frame does. */
/* Two fixed states, not a fluid range. The artwork holds the 350 frame's 64px
   until the widget crosses the breakpoint, then holds the 1280 frame's 132px.
   Scaling it continuously with cqw made it drift to sizes that appear in
   neither frame, which read as wrong at every width in between. */
@container (min-width: 560px) {
  .body {
    grid-template-columns: 132px 1fr;
    grid-template-areas: "art meta" "art controls";
    /* 16px, not 21. The podcast frame's title block starts at y=5 and ends at
       51 with controls at 67, so the gap is 16 and the 5 is padding above the
       title. Counting the 5 as part of the gap pushed everything down. */
    column-gap: 16px; row-gap: 16px; align-items: start;
  }
  .art { width: 132px; height: 132px; }
  .art-link { align-self: start; }
  /* .body .meta, not .meta. This 5px is the bar card's own top offset against
     its 132px artwork, and unscoped it also reached the artwork cards, whose
     .meta sits in .topbar. There it added 5px to a 36px text block beside a
     40px tile, so the block came out 41 tall and hung off the artwork instead
     of centring on it. Only visible above the 560 breakpoint, which is why it
     measured clean on a phone. */
  .body .meta { padding-top: 5px; }
  /* Straight off frame 2526:135975: title 16/24 SemiBold, subtitle 14/18
     Regular, both at -0.2 letter spacing. An earlier pass guessed 20/24 and
     -0.4, which made the podcast title noticeably larger than the design. */
  /* subtitle-2 over caption-2 at the wide size, the same pairing the narrow
     size makes with subtitle-4 over caption-4. */
  .title { font-size: var(--fs-16); line-height: var(--lh-24); letter-spacing: var(--ls-0); }
  .subtitle { font-size: var(--fs-14); line-height: var(--lh-18); letter-spacing: var(--ls-0); }
  /* The live frames keep the now-playing block at 14/18 and the station line
     at 12/16 at BOTH sizes, so the wide rules must not scale them up. */
  .widget.live .title { font-size: var(--fs-14); line-height: var(--lh-18); letter-spacing: var(--ls-0); }
  .widget.live .subtitle { font-size: var(--fs-12); line-height: var(--lh-16); }

  /* Live centres the text-plus-controls group against the 132px artwork, and
     it is the GROUP that centres, not each row on its own. With a track the
     group is 56 + 16 + 60 = 132 and fills the artwork exactly, which is why
     frame 2527:144307 shows the text at y=0 and the controls at y=72. With no
     track it is 16 + 16 + 60 = 92, and frame 2530:61496 puts it at y=20, the
     centred position. The two-row grid could not do this: the artwork spans
     both rows, so its surplus height was split evenly between them, which
     pushed the text to the top and opened the gap under it to 33.5px. One flex
     column, centred as a unit, is the layout the frames actually describe.
     Podcast keeps the two-row grid, since its own frame offsets it from the
     top rather than centring it. */
  .widget.live .body { grid-template-areas: "art col"; }
  .widget.live .col {
    grid-area: col; align-self: stretch; min-width: 0;
    display: flex; flex-direction: column; justify-content: center; gap: 16px;
  }
  /* The 5px is the podcast frame's top offset. Live centres instead. */
  .widget.live .meta { padding-top: 0; }
}

.controls-row { grid-area: controls; display: flex; gap: 16px; align-items: center; width: 100%; min-width: 0; }
@container (min-width: 560px) { .controls-row { align-items: flex-start; } }
/* Live has no scrubber, so its single control group centres on the play button
   rather than hanging off the top. The frame puts the 32px group at y=14 of a
   60px row, which is exactly centred. */
.widget.live .controls-row { align-items: center; }

/* The play button shrinks rather than hanging off the card.

   60 is the frame's size and it is what the button is at any width that can
   hold it. Below that it takes whatever is left instead: it sits 16 in from the
   left edge, so `100cqw - 20px` keeps 4px clear on the right, and the min()
   means the rule does nothing at all until the card is under 80 wide, which is
   exactly where the measurement said it starts to run off. At 50 the button is
   30 and clears the edge by 4.

   `cqw` resolves against .shell, which is the only inline-size container on the
   page, so it is the card's own width and not the viewport's.

   The padding and the glyph are ratios of the whole rather than fixed pixels,
   0.0625 and 0.875, which are the frame's own 3.75 and 52.5 over 60. Left as
   pixels they would have overflowed the button the moment it shrank. */
.play-btn {
  --play: min(60px, 100cqw - 20px);
  appearance: none; border: 0; cursor: pointer; background: var(--true-white);
  border-radius: 999px; display: flex; flex: 0 0 auto;
  width: var(--play); height: var(--play);
  padding: calc(var(--play) * .0625); box-sizing: border-box;
  position: relative;
  filter: drop-shadow(0 .938px 1.406px rgba(0,0,0,.15));
}
/* Buffering ring, copied from iheart.com rather than approximated.
   The spinner itself is packages/accomplice/src/icons/loading in
   iheartradio/web: a circle of r=47 in a 100 viewBox, round cap,
   stroke-dasharray 60 300 so the arc covers about a fifth of the 295 unit
   circumference, stroke-width 8 user units, rotating once every .75s linear,
   stroked in gray300.
   How it is MOUNTED matters as much as the drawing, and is the part an earlier
   pass got wrong. In button.tsx a pending icon button renders the loader with
   LOADER_BOX_PROPS_ICON, which is position absolute, inset 0, width and height
   100%, laid OVER the button with the glyph still visible underneath. So the
   ring is the button's own diameter, not a larger ring around it, and its
   stroke is 8/100 of the button, 4.8px on this 60px button. The earlier version
   sat 7px outside the button at a 3.3px stroke, which is why it read as a
   different spinner. */
.spin { position: absolute; inset: 0; width: 100%; height: 100%;
        display: none; pointer-events: none; overflow: visible; }
.widget.buffering .spin { display: block; }
.spin circle {
  fill: transparent; stroke: var(--ihr-grey-300); stroke-width: 8;
  stroke-dasharray: 60 300; stroke-linecap: round;
  transform-origin: center; animation: spin .75s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Still turns, because a stopped ring would read as a hung player, but slowly
   enough not to be the flicker the preference is asking to avoid. */
@media (prefers-reduced-motion: reduce) { .spin circle { animation-duration: 2.4s; } }
.play-btn img { width: calc(var(--play) * .875); height: calc(var(--play) * .875); display: block; }
/* kind `primary`, color `white`: brandWhite at rest with a gray600 glyph, and
   gray300 for both hover and pressed. The hover half sits inside
   (pointer: fine) in the original, so a touch device never paints it. */
.play-btn { color: var(--ihr-grey-600); box-shadow: var(--elevation1); }
@media (pointer: fine) {
  .play-btn:hover:not([disabled]) { background-color: var(--ihr-gray-300); }
}
.play-btn:active { background-color: var(--ihr-gray-300); }
.stack { display: flex; flex-direction: column; gap: 8px; flex: 1 1 0; min-width: 0; }

/* ---------- the scrubber, which is the accomplice Slider ----------
   This is the component the design system's Seek Bar story renders, rebuilt
   from packages/accomplice/src/components/slider/slider.css.ts in the web
   monorepo rather than eyeballed off the Storybook page, so the numbers are the
   component's own:

     root        flex row, 12 gap, centred, 100% wide
     track       position relative, min-height 16, and a ::before line rather
                 than a padded box, so the hit area and the line are separate
                 things instead of one trick
     track size  2px, and 3px from 1025 up, which is the large breakpoint
     line        6px radius, centred on the track with top 50% and a translate
     fill        red550, red400 while the track is hovered, red650 while dragging
     preview     the segment between the thumb and where a click would land,
                 drawn only while hovering ahead of the current position
     thumb       12px, red550 with the same two states, elevation1 shadow, and
                 HIDDEN until the track is hovered or the thumb is dragging,
                 which is the component's onlyThumbOnHover default

   The colour pairs are lightDark() in the original. These cards are dark
   surfaces in every state, so they take the dark half of each pair: grey450
   for the line, grey250 for the preview. 1.2rem reads as 12px because
   accomplice sets a 10px rem base, which its own space scale confirms, where
   space[12] is 1.2rem. */
.slider { display: flex; flex-direction: row; position: relative; gap: 12px;
          justify-content: center; align-items: center; width: 100%; max-width: 100%; }
/* caption-4 for the time labels. Tabular figures are not part of the kind and
   stay local: a clock that changes width as it counts is its own problem. */
.slider .t { font-size: var(--fs-12); line-height: var(--lh-16); letter-spacing: var(--ls-4);
             color: var(--true-white); white-space: nowrap; font-variant-numeric: tabular-nums; }
.track {
  --track-size: 2px;
  position: relative; flex: 1 1 0; min-width: 0; width: 100%; min-height: 16px;
  cursor: pointer; touch-action: none;
}
@media screen and (min-width: 1025px) { .track { --track-size: 3px; } }
.track::before {
  content: ''; position: absolute; top: 50%; transform: translateY(-50%);
  left: 0; width: 100%; max-width: 100%; height: var(--track-size);
  border-radius: 6px; background: var(--ihr-grey-450);
}
/* Fill and preview share the line's geometry, which is why it is factored out
   in the original too. */
.elapsed, .preview {
  position: absolute; top: 50%; transform: translateY(-50%);
  height: var(--track-size); max-width: 100%; border-radius: 6px;
}
.elapsed { left: 0; width: 0; background: var(--ihr-red-550); }
.track[data-hovered] .elapsed:not([data-dragging]) { background: var(--ihr-red-400); }
.elapsed[data-dragging] { background: var(--ihr-red-650); }
.preview { width: 0; background: var(--ihr-grey-250); }
.thumb {
  position: absolute; top: 50%; left: 0; width: 12px; height: 12px;
  margin: -6px 0 0 -6px; border-radius: 50%; background: var(--ihr-red-550);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .15); forced-color-adjust: none;
  display: none;
}
.track[data-hovered] .thumb, .thumb[data-dragging] { display: block; }
.track[data-hovered] .thumb:not([data-dragging]) { background: var(--ihr-red-400); }
.thumb[data-dragging] { background: var(--ihr-red-650); }
.thumb[data-focus-visible] { outline: 2px solid blue; }

.btn-row { display: flex; align-items: center; justify-content: space-between; width: 100%; gap: 8px; }
/* Live has a single control group and the frame puts it at x=0, so it is left
   aligned rather than pushed to the far edge. The empty left group it used to
   render was still a flex item, so the row's 8px gap sat in front of the
   icons and pushed them to 24px from the play button where the frame says 16.
   An empty element is not free in a gapped flex row. */
.widget.live .btn-row { justify-content: flex-start; }
.btn-group { display: flex; gap: 8px; align-items: center; }
/* ---------- accomplice Button ----------
   baseButtonStyles, then size `icon`, then the compound variant each button
   actually is. Read from button.css.ts and button/size.ts rather than invented:
   the wash this page used to paint on hover, rgba(255,255,255,.14), is not a
   value the system has anywhere.

   Three things about the real states are easy to get wrong:

   1. The BASE has no hover background at all. Hover is `cursor: pointer` and
      nothing else. Every background belongs to a compound variant.
   2. For the coloured variants the hover background lives inside
      `@media (pointer: fine)`, so a touch device gets no hover state. Only the
      neutral tertiaries (white, default, gray) hover unconditionally.
   3. Hover and pressed are the SAME colour. The pressed state is told apart by
      `transform: scale(0.95)`, not by a darker fill. */
.icon-btn, .h-btn, .play-btn, .hero-play {
  appearance: none; border: 0; position: relative;
  display: inline-flex; flex-direction: row; align-items: center; justify-content: center;
  gap: var(--sp-4); border-radius: var(--rad-999);
  text-decoration: none; user-select: none; overflow: visible; pointer-events: all;
}
@media (pointer: fine) {
  .icon-btn, .h-btn, .play-btn, .hero-play { transform: scale(1); transition: 300ms; }
}
.icon-btn:hover, .h-btn:hover, .play-btn:hover, .hero-play:hover { cursor: pointer; }
/* data-pressed in the original, which is react-aria's held state. :active is
   the plain CSS equivalent. 0.95, not the 0.96 this page had. */
.icon-btn:active, .h-btn:active, .play-btn:active, .hero-play:active { transform: scale(.95); }
.icon-btn:focus-visible, .h-btn:focus-visible,
.play-btn:focus-visible, .hero-play:focus-visible {
  transition: none; outline-style: solid; outline-width: var(--border-2);
  outline-offset: 2px; outline-color: var(--ihr-blue-400);
}
.icon-btn:focus-visible:not(:active), .h-btn:focus-visible:not(:active),
.play-btn:focus-visible:not(:active), .hero-play:focus-visible:not(:active) { transform: scale(1); }

/* ---------- accomplice Tooltip ----------
   From components/tooltip/tooltip.css.ts and tooltip.tsx. The text is
   attr(aria-label) rather than a second attribute, so the tooltip and the
   accessible name are the same string by construction and cannot drift. The
   labels are already kept current as state changes, so the play button reads
   Play, Pause or Stop without any extra wiring.

   The colours are lightDark(gray600, brandWhite) for the surface and
   lightDark(brandWhite, gray600) for the text. These cards are dark surfaces,
   so the tooltip takes the dark half of each: a white bubble with gray600 text.
   That is also why only the buttons ON the card carry one. The drawer's close
   button sits on white, where a white bubble would be invisible, and it is
   excluded by selector rather than by accident.

   One deviation, stated rather than hidden: the component renders its arrow as
   an <svg> with the path M0 0 L8 8 L16 0. There is no element to put that in
   here, so the same path is inlined as a data URI on a pseudo-element. The
   geometry is the component's; the delivery is not. */
.icon-btn, .h-btn, .play-btn, .hero-play { position: relative; }
.icon-btn[aria-label]::after, .h-btn[aria-label]::after,
.play-btn[aria-label]::after, .hero-play[aria-label]::after {
  content: attr(aria-label);
  /* 8, not 11. The beak has to overlap the bubble by more than the shadow
     spreads, or the shadow draws a line across the join. elevation1 is
     0 1px 3px, so 4px of overlap covers it and the 1px border under it.
     The beak's tip stays 4 from the button either way. */
  position: absolute; bottom: calc(100% + 8px); left: 50%;
  background-color: var(--true-white); color: var(--ihr-grey-600);
  border: 1px solid var(--ihr-gray-350); border-radius: var(--rad-2);
  box-shadow: var(--elevation1);
  padding: var(--sp-4) var(--sp-8);
  font-size: var(--fs-10); line-height: var(--lh-14);
  letter-spacing: var(--ls-0); font-weight: 400;
  white-space: nowrap; forced-color-adjust: none;
}
.icon-btn[aria-label]::before, .h-btn[aria-label]::before,
.play-btn[aria-label]::before, .hero-play[aria-label]::before {
  content: ''; position: absolute; bottom: calc(100% + 4px); left: 50%;
  width: var(--sp-16); height: var(--sp-8);
  background: no-repeat center/16px 8px
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='8'%3E%3Cpath d='M0 0 L8 8 L16 0' fill='%23ffffff' stroke='%23ffffff' stroke-width='1' stroke-linecap='round'/%3E%3C/svg%3E");
}
/* The beak paints OVER the bubble. Same z-index meant ::after won on document
   order, so the bubble and its shadow were drawn on top of the beak and the
   join showed as a grey line. iheart.com has no such line, because there the
   arrow is a child of the tooltip rather than a second box behind it. */
.icon-btn[aria-label]::before, .h-btn[aria-label]::before,
.play-btn[aria-label]::before, .hero-play[aria-label]::before { z-index: 7; }
/* Shared rest and motion. The component enters over 200ms from an 8px offset
   and leaves over 100ms, reversed and eased out. */
.icon-btn[aria-label]::after, .h-btn[aria-label]::after,
.play-btn[aria-label]::after, .hero-play[aria-label]::after,
.icon-btn[aria-label]::before, .h-btn[aria-label]::before,
.play-btn[aria-label]::before, .hero-play[aria-label]::before {
  opacity: 0; pointer-events: none; z-index: 6;
  transform: translateX(-50%) translateY(var(--sp-8));
  /* Leaving: wait the close delay, then fade out. react-aria calls these delay
     and closeDelay and they are separate from the animation durations, which
     the component fixes at 200ms in and 100ms out. accomplice's own wrapper
     sets neither, so every call site picks its own; 200 and 100 is what
     number-field and textarea use, the two closest things to a compact control
     whose icon is doing the explaining. 200 is long enough that sweeping the
     cursor across a transport row does not flash six tooltips, and short enough
     that a deliberate hover still feels immediate. */
  transition: opacity 100ms ease-out, transform 100ms ease-out;
  transition-delay: 100ms;
}
.icon-btn[aria-label]:hover::after, .h-btn[aria-label]:hover::after,
.play-btn[aria-label]:hover::after, .hero-play[aria-label]:hover::after,
.icon-btn[aria-label]:hover::before, .h-btn[aria-label]:hover::before,
.play-btn[aria-label]:hover::before, .hero-play[aria-label]:hover::before,
.icon-btn[aria-label]:focus-visible::after, .h-btn[aria-label]:focus-visible::after,
.play-btn[aria-label]:focus-visible::after, .hero-play[aria-label]:focus-visible::after,
.icon-btn[aria-label]:focus-visible::before, .h-btn[aria-label]:focus-visible::before,
.play-btn[aria-label]:focus-visible::before, .hero-play[aria-label]:focus-visible::before {
  opacity: 1; transform: translateX(-50%) translateY(0);
  transition: opacity 200ms, transform 200ms;
  transition-delay: 200ms;
}
/* A hidden spacer is still a button. It has no label and no tooltip, but say so
   rather than rely on it. */
.h-btn.spacer::after, .h-btn.spacer::before { content: none; }
/* react-aria shifts an overlay back inside the viewport when it would overhang.
   A pseudo-element has no such machinery, and design C's bottom row pins groups
   to BOTH edges, so buttons at either end need it: they sit 16 from the card's
   edge, and any bubble wider than twice that overhangs at every width rather
   than only at narrow ones. "Show episodes" is 83 and "Playback speed" is 86.
   So each end group anchors to its own edge instead of centring, which is where
   react-aria would put them. The arrow stays centred on the button either way.
   Scoped per group, because anchoring the left group's bubbles to their right
   edge would push them further off the card rather than back on. */
.lr-side:first-child .h-btn[aria-label]::after {
  left: 0; right: auto;
  transform: translateX(0) translateY(var(--sp-8));
}
.lr-side:last-child .h-btn[aria-label]::after {
  left: auto; right: 0;
  transform: translateX(0) translateY(var(--sp-8));
}
.lr-side .h-btn[aria-label]:hover::after,
.lr-side .h-btn[aria-label]:focus-visible::after {
  transform: translateX(0) translateY(0);
}
@media (prefers-reduced-motion: reduce) {
  .lr-side .h-btn[aria-label]::after { transform: translateX(0) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .icon-btn[aria-label]::after, .h-btn[aria-label]::after,
  .play-btn[aria-label]::after, .hero-play[aria-label]::after,
  .icon-btn[aria-label]::before, .h-btn[aria-label]::before,
  .play-btn[aria-label]::before, .hero-play[aria-label]::before {
    /* The delays stay. They are about intent, not motion, and removing them
       would make the tooltips MORE eager for someone who asked for less. */
    transition: opacity 100ms; transform: translateX(-50%) translateY(0);
  }
}

/* size `icon`: padding space[4], aspect-ratio 1/1, radius[999]. */
.icon-btn, .h-btn { padding: var(--sp-4); aspect-ratio: 1 / 1; background: transparent; }
.icon-btn img { width: 24px; height: 24px; display: block; }

/* kind `tertiary`, color `white`, on a dark surface. Hover and pressed take
   gray400, and so does the sustained open state, which the component expresses
   as [data-persist-pressed-state][aria-expanded="true"] and this page as
   aria-pressed. Not gated on pointer: the neutral tertiaries hover everywhere. */
.icon-btn { color: var(--true-white); }
.icon-btn:hover, .icon-btn:active, .icon-btn[aria-pressed="true"] {
  background-color: var(--ihr-gray-400);
}
/* accomplice Button at size `small`: the button-2 kind on a space[32] height.
   The padding the component pairs with it is 8 16, which a two character label
   in a control row cannot afford, so the height and the kind carry over and the
   padding stays local. */
.speed-btn { min-width: var(--sp-32); height: var(--sp-32);
  align-items: center; justify-content: center; color: var(--true-white);
  font-size: var(--fs-14); line-height: var(--lh-16); font-weight: 600;
  letter-spacing: var(--ls-0); font-family: inherit; }

/* Bars rise from the BOTTOM with rounded tops. Figma authors them as
   items-start with rounded-bl/br inside a wrapper carrying -scale-y-100;
   reading the bar styling without that flip gives the mirror image. */
/* Bars hold the design's width and the COUNT grows with the container. The
   Figma frames use 3px bars at 350 (70 of them, flexed) and 4px at 1280, so
   flexing a fixed 70 bars across a wide widget ballooned them to 15px, several
   times the design. Width is fixed here and the count is derived instead. */
.wave { display: flex; gap: var(--bar-gap, 2px); height: 16px; width: 100%; align-items: flex-end;
        opacity: .8; overflow: hidden; }
.wave i { flex: 0 0 var(--bar-w, 3px); width: var(--bar-w, 3px); display: block;
          background: var(--ihr-grey-300); border-radius: 2px 2px 0 0; height: 3px;
          transition: height 90ms linear; }

/* No bottom padding. In the frame the rows region runs from y=48 to the list's
   own bottom edge at 220, a 172px viewport, and the rows are clipped there
   rather than at a padding edge. Reserving 16px at the bottom shrank that to
   156px, which left the third row peeking by only 8px instead of 24 and so
   read as if the list simply ended. */
.list { width: 100%; height: 220px; background: var(--ihr-grey-150); padding: 12px 16px 0;
        display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
/* Everything in the list box that is not a 72px row, plus the 16px peek the
   stepped modes keep under the last one. 12 of top padding + a 32 header + the
   4 gap under it + the 16 peek.

   Declared on the WIDGET, not on .list, which is where it is measured from and
   where it would read better. --list-h is computed on the widget and a custom
   property only inherits downward, so a value set on the list is invisible to
   the rule that needs it and silently falls back. */
.widget.c { --list-fix: 64px; }
/* The episode list has no heading. Its 32 header and the 4 gap under it are the
   36 that come off. */
.widget.c[data-kind="podcast"] { --list-fix: 28px; }
/* The frame's list header is a 32px row (it carries a 32px Buttons frame
   beside the label), so rendering it at the text's own 18px line-height closed
   the gap under "Episodes" by 14px. */
.list h3 { margin: 0; flex: 0 0 32px; height: 32px; display: flex; align-items: center;
           font-size: var(--fs-14); line-height: var(--lh-18);
           letter-spacing: var(--ls-0); font-weight: 600; }
.rows { display: flex; flex-direction: column; gap: 2px; overflow-y: auto; }
/* No fade over the last row, and no forced peek under it. Both were there to
   hint that the list scrolled; the drawn scrollbar says that outright, and a
   mask over a list that ends where it ends was softening a cut that no longer
   needs making. */
/* The overflow button is a sibling of the row, not a child of it, because the
   row carries role="button" and a control nested inside one is not reliably
   reachable. The wrapper is what lets them sit on top of each other. */
.row-wrap { position: relative; flex: 0 0 auto; }
/* Frame 2609:36099: 32 square, centred in the 72px row, right edge 12 in. */
.row-more {
  position: absolute; right: 12px; top: 50%; transform: translateY(-50%);
  appearance: none; border: 0; background: transparent; cursor: pointer;
  width: 32px; height: 32px; padding: 4px; border-radius: var(--rad-999);
  display: flex; align-items: center; justify-content: center;
  transition: background-color .2s ease;
}
.row-more img { display: block; width: 24px; height: 24px; }
.row-more:hover, .row-more:active { background-color: rgba(0, 0, 0, .08); }
.row-more:active { transform: translateY(-50%) scale(.95); }
.row-more:focus-visible { outline: 2px solid var(--ihr-red-550); outline-offset: -2px; }
/* Room for it, so a long title ellipsises before it runs underneath. */
.row-wrap .row { padding-right: 52px; }
/* The link out has no overflow button, so it reserves nothing and its glyph
   sits where theirs do. Written here rather than beside the rest of the
   row-more-link rules because `.row-wrap .row` above is the same specificity
   and comes later, so an override further up the file quietly lost. */
.row-wrap .row.row-more-link { padding-right: 16px; }
.row { display: flex; gap: 12px; align-items: center; height: 72px; padding: 8px 12px;
       border-radius: var(--radius-widget); cursor: pointer; }
.row:hover { background: rgba(0,0,0,.05); }
.row:focus-visible { outline: 2px solid var(--ihr-red-550); outline-offset: -2px; }
/* Now playing is signalled by the title turning brand red, not by a filled
   row. The filled bar read as a selection state rather than playback. */
.row.on { background: none; }
/* brandRed on the loaded episode. */
.row.on .row-meta b { color: var(--ihr-brand-red); }
/* ---------- the row artwork, with its play control ----------
   From apps/listen/app/components/row/row-image.tsx, where Row clones the play
   button into RowImage as a child. Two things fade in together over 300ms:
   a brandBlack scrim at 0.2, and the button itself from 0 to 1.

   The trigger there is `isHovering || isActive || isMenuOpen`, and `active` is
   a semantic prop meaning this row is the current item, not a press state. So
   the current episode keeps its control on screen while every other row reveals
   one on hover, which is exactly what .row.on already marks here.

   RowImage also sets pointer-events and display to none under pointerCoarse.
   A touch device has no hover, so an overlay that only appears on hover would
   either never show or never leave; the row itself stays tappable. */
.row-art { position: relative; flex: 0 0 auto; width: 56px; height: 56px;
           aspect-ratio: 1 / 1; border-radius: var(--rad-6);
           box-shadow: var(--elevation1); }
/* A face, not a sleeve. Artist radio and playlist list Featured Artists and
   nothing else, so every row art on those two cards is a person and takes the
   circle the artist tile in the top bar already takes.

   Keyed on the card's kind rather than on `.row.inert`, which is the other thing
   that identifies these rows: that class is scoped to the inline list, so it
   would have left the drawer's copy of the same list square. */
.widget.c[data-kind="artist"] .row-art,
.widget.c[data-kind="playlist"] .row-art { border-radius: 50%; }
.row-scrim {
  position: absolute; inset: 0; z-index: 1; border-radius: var(--rad-6);
  background-color: #000; opacity: 0;
  transition: opacity ease 300ms; pointer-events: none;
}
.row-play {
  position: absolute; left: 50%; top: 50%; z-index: 2;
  transform: translate3d(-50%, -50%, 0);
  opacity: 0; transition: opacity ease 300ms; pointer-events: none;
  /* Button color default, kind primary, size icon: brandWhite behind a gray600
     glyph on a light surface, padding space[4], square, fully round. */
  background-color: var(--true-white); color: var(--ihr-grey-600);
  padding: var(--sp-4); aspect-ratio: 1 / 1; border-radius: var(--rad-999);
  box-shadow: var(--elevation1);
  display: flex; align-items: center; justify-content: center;
}
.row-play img { width: 24px; height: 24px; display: block; }
.row:hover .row-scrim, .row.on .row-scrim { opacity: .2; }
.row:hover .row-play, .row.on .row-play { opacity: 1; }
/* The glyph follows playback: only the current row can be the one playing. */
.row-play .pi-pause { display: none; }
.widget.playing .row.on .row-play .pi-play { display: none; }
.widget.playing .row.on .row-play .pi-pause { display: block; }
@media (pointer: coarse) {
  .row-scrim, .row-play { display: none; }
}
.tile { width: 56px; height: 56px; border-radius: var(--radius-widget); object-fit: cover;
        background-image: linear-gradient(153.68deg, #c5cdd2 0%, #717277 100%);
        filter: drop-shadow(0 1px 1.5px rgba(0,0,0,.15)); }
.row-meta { display: flex; flex-direction: column; justify-content: center; flex: 1 1 0; min-width: 0; }
.row-meta b { font-size: var(--fs-14); line-height: var(--lh-18);
              letter-spacing: var(--ls-0); font-weight: 600;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* `> span`, not a descendant `span`. The subtitle is a direct child of
   .row-meta, but every marquee wraps its text in a .mqi span, so a descendant
   selector also caught the one INSIDE the <b> and restyled the title with the
   subtitle's rules. That is what actually paints the text, so every episode
   title was rendering at 12/16 in grey-450 rather than 14/18 in its own colour,
   and the loaded episode's brand red never reached the screen at all: the
   colour was correct on the <b> and overwritten on the span inside it. */
.row-meta > span { font-size: var(--fs-12); line-height: var(--lh-16);
                 letter-spacing: var(--ls-4); color: var(--ihr-grey-450);
                 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The accomplice Badge rule that lived here is gone with the explicit marker on
   the episode rows. It was fontSize[11] / lineHeight[16] / weight 700,
   letterSpacing[5], uppercase, radius[2], at the small variant's 0 4 padding.
   The Figma rows still draw one; the player that ships today does not, and that
   is what settled it. Nothing else on the page rendered a .badge, so the rule
   went with the markup rather than sitting here unused. */

.status { font-size: 12px; color: var(--ihr-grey-450); min-height: 18px; margin: 8px 0 0; }
.status.err { color: #b00020; font-weight: 600; }
.colourline { font: 11.5px ui-monospace, Menlo, monospace; color: var(--ihr-grey-450); display: flex; align-items: center; gap: 6px; margin-top: 6px; }
.chip { width: 22px; height: 14px; border-radius: 3px; border: 1px solid rgba(0,0,0,.15); display: inline-block; }
.ratio { font-weight: 600; color: var(--ihr-grey-600); }
.wcag { font: 600 10.5px/1 'Open Sans', sans-serif; letter-spacing: .04em; text-transform: uppercase;
        padding: 3px 6px; border-radius: 3px; }
.wcag.pass { background: rgba(20,140,80,.14); color: #14814f; }
.wcag.warn { background: rgba(200,120,0,.16); color: #8a5200; }
.wcag.fail { background: rgba(176,0,32,.12); color: #b00020; }
.meta-note { color: var(--ihr-grey-450); }

/* ---------- accomplice Toast, kind=info ----------
   A Toast renders a Notification, so the visual spec is
   components/notification/notification.css.ts and the layout is its render:

     root       radius space[6], min-height 5.6rem, padding space[16],
                background blue100 for kind=info
     grid       "icon content", 8 column gap, items centred
     icon       blue600, aligned to the start rather than centred
     head       "copy close", 8 column gap, items start
     copy       gray600, body-4, and the component hard-sets a 2.4rem line
                height inline, so that wins over the kind's own 18
     close      Button size icon, kind tertiary, color gray, nudged up 0.3rem
     actions    8 column gap, justified to the end, 8 of padding above

   SIZE. Accomplice's Notification steps its type up at shmedium, 560, and the
   skip-limit caller steps its CTA up at medium, 769. Neither step is here. The
   toast is pinned to the smaller of each pair at every card width, which is
   what the card was asked for, and it is also the size that suits the surface,
   since the toast sits inside a player that is usually far narrower than the
   page around it. Every value below is accomplice's small one, unchanged.

   The region. The global one is fixed to the viewport, 500px wide from
   shmedium up. An embed has no viewport worth speaking of, so this one is
   absolute inside the card that raised it, which is also the only honest place
   for it: the toast belongs to that player, not to the page hosting it. */
.toast-region {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 8;
  display: flex; justify-content: center;
  padding: var(--sp-16); pointer-events: none;
}
.toast {
  width: 100%; max-width: 500px; pointer-events: auto;
  border-radius: var(--sp-6); min-height: 56px; padding: var(--sp-16);
  background-color: var(--ihr-blue-100);
  display: grid; grid-template-columns: auto 1fr;
  column-gap: var(--sp-8); align-items: center;
  box-shadow: var(--elevation1);
  animation: toast-in 200ms ease-out;
}
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }
.toast-icon { align-self: start; display: block; width: 24px; height: 24px; }
.toast-body { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.toast-head {
  display: grid; grid-template-columns: 1fr auto;
  column-gap: var(--sp-8); align-items: start;
}
/* Accomplice's Notification title, subtitle-2 at its xsmall breakpoint:
   16 / 600 / 24 with letterSpacing[0], which is -0.02rem at its 10px rem base
   and so -0.2px here. */
.toast-text { display: flex; flex-direction: column; gap: var(--sp-2); min-width: 0; }
.toast-title {
  color: var(--ihr-grey-600); margin: 0;
  font-size: var(--fs-16); line-height: var(--lh-24); font-weight: 600;
  letter-spacing: var(--ls-0);
}
/* Accomplice's Notification copy, body-4 at its xsmall breakpoint: 14 / 400
   with letterSpacing[1], -0.05rem there and so -0.5px here. The 24 line height
   is not body-4's own 18. Notification hard-sets lineHeight 2.4rem inline on
   this span at EVERY breakpoint, so 24 is what the component actually renders.

   It used to step to body-3, 16, at 560. It does not any more: the toast is
   pinned to accomplice's small size at every card width. */
.toast-copy {
  color: var(--ihr-grey-600);
  font-size: var(--fs-14); line-height: 24px; font-weight: 400;
  letter-spacing: var(--ls-1); margin: 0;
}
.toast-close {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  padding: var(--sp-4); aspect-ratio: 1 / 1; border-radius: var(--rad-999);
  display: inline-flex; align-items: center; justify-content: center;
  position: relative; top: -3px; color: var(--ihr-grey-600);
}
.toast-close img { width: 24px; height: 24px; display: block; }
.toast-close:hover, .toast-close:active { background-color: var(--ihr-gray-300); }
.toast-actions {
  display: flex; column-gap: var(--sp-8); justify-content: flex-end;
  padding-top: var(--sp-8);
}
/* Button size small, which is where the CTA now stays at every card width.
   The skip-limit caller asks for { medium: 'large', xsmall: 'small' }, so its
   large size arrives at medium, 769, not at shmedium. Either way the toast is
   pinned to small now, so the step is gone rather than corrected.

   Accomplice's small: height space[32], padding space[8] space[16], button-2,
   which is 14 / 600 / 16 with letterSpacing[0], -0.2px here. Tertiary and gray
   with gray600 text, per getAuthCTAToastProps. */
.toast-action {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  color: var(--ihr-grey-600); text-decoration: none;
  height: var(--sp-32); padding: var(--sp-8) var(--sp-16);
  border-radius: var(--rad-999);
  display: inline-flex; align-items: center; justify-content: center;
  font-family: inherit; font-size: var(--fs-14); line-height: var(--lh-16);
  font-weight: 600; letter-spacing: var(--ls-0);
}
.toast-action:hover, .toast-action:active { background-color: var(--ihr-gray-300); }
.toast-close:focus-visible, .toast-action:focus-visible {
  outline: var(--border-2) solid var(--ihr-blue-600); outline-offset: 2px;
}

/* ---------- accomplice Drawer, the share sheet ----------
   This was a Dialog, and a Dialog was the wrong component for it inside a card.
   Its close button is absolute at top -40, which works on a full page where the
   modal is centred with room above it, and fails completely in a box that clips
   its own overflow: the close landed outside the card on five of six designs,
   so the sheet could not be closed at all.

   A Drawer has nowhere to put a button outside itself. Its heading is a real
   bar inside the surface, 5.6rem tall and justified space-between, which is
   where the close belongs. From components/drawer/drawer.css.ts:

     overlay   rgba(0 0 0 / 0.8) with a 10px backdrop blur, over 600ms
     drawer    right, bottom, left 0, width 100%, max-height 80vh,
               radius 6px 6px 0 0, sliding up from translateY(100%) over 600ms
     heading   5.6rem tall, space-between, gray200 on light, 18px at
               letterSpacing[1], 16 of side padding
     body      overflow auto, 16 / 24

   The light half of each pair, matching the episodes drawer already on these
   cards. Built the same way as that drawer too: rendered into the card and
   toggled by a class, not appended at click time. Appending fought the card,
   because an inset:0 overlay resolves against a box that is not the one you
   see, and the sheet escaped the card entirely. 80% rather than 80vh, since
   the card is what it must fit, not the viewport. */
/* The scrim LEADS the panel: 250ms against the panel's 600.

   accomplice fades its overlay over the same 600ms the drawer slides in, and
   that is right where the overlay stays visible AROUND the panel. Here the
   panel covers the whole card, so the overlay's only job is the moment before
   it lands, and matching the durations meant the card was still at half
   brightness when the sheet was already halfway up it. Measured on design B:
   the panel was at 211 of 444 while the scrim was at 0.52.

   Design B is where that read as wrong, because its card is 444 tall against
   design C's 234, so the sheet spends the first half of its journey crossing
   the episode list. Same 600ms, same curve, no dropped frames on either, just
   nearly twice the distance. Dimming first puts the sheet over a backdrop that
   has already receded, which is what a drawer is supposed to look like. */
.share-scrim {
  position: absolute; inset: 0; z-index: 19;
  /* No blur, and the reason is that it was never visible.

     The share drawer is the only one of the three with an overlay, and the
     episode drawer beside it is the one whose motion reads right. The panels
     themselves are byte for byte identical: same 600ms, same curve, same
     `will-change`, same starting transform, all three staged the same way. The
     overlay was the whole difference.

     accomplice's `overlayFade` ramps `blur(0)` to `blur(10px)`, and a blur
     radius that changes every frame is the one thing a compositor cannot cache.
     That cost bought nothing here: this panel is `height: 100%` of a container
     at `inset: 0`, so once it lands it covers the card completely. Checked by
     hit testing the four corners and the centre with the drawer open, and the
     topmost element is the panel at every one of them. The overlay is therefore
     only ever seen DURING the slide, which is exactly when the blur was making
     the slide worse.

     The colour dim stays, because that part is close to free and it is what
     makes the card recede while the sheet crosses it. */
  /* No dim. Measured at the same point in a slowed slide, the share panel and
     the episode panel sit in exactly the same place and travel for exactly the
     same 500ms with the same easing, so the transform was never the difference.
     What differed was this: the strip of card still showing above the panel was
     BLACK on the share drawer and the artwork on the other two, so share read
     as a dark wipe and the others as a panel sliding over a card.

     accomplice's Dialog has a scrim and its Drawer does not, which is why they
     diverged. Consistency across the three drawers on one card matters more
     here than which component each was lifted from. The element stays, because
     it is the click away target. */
  background-color: transparent;
  visibility: hidden;
  transition: visibility 0s linear .6s;
}
.widget.share-open .share-scrim {
  visibility: visible;
  transition: visibility 0s;
}

/* inset:0 with the drawer nested inside, rather than the drawer itself anchored
   with bottom:0. The card's own drawers all position this way and all work;
   bottom:0 does not, because on the bar card .widget wraps the player AND the
   episode list, so a sheet anchored to its bottom lands under the list. */
.share-sheet {
  position: absolute; inset: 0; z-index: 20;
  display: flex; flex-direction: column; justify-content: flex-end;
  overflow: hidden; outline: none; pointer-events: none;
  visibility: hidden; transition: visibility 0s linear .6s;
}
.widget.share-open .share-sheet { visibility: visible; transition: visibility 0s; }

/* ---------------------------------------------------------------------------
   The staged open, for EVERY drawer.

   openStaged() in widget-core.js adds this class for two frames before it flips
   the open flag, so the panel is painted at its closed position first and the
   slide starts from something already on screen. The class was being added and
   removed and NOTHING responded to it: this rule was lost in an earlier edit,
   so the staging had been a no-op and each drawer began its slide on whatever
   frame the browser happened to paint it.

   That is what made the share drawer arrive differently from the other two. It
   carries a scrim and an input and paints heavier than the episode list, so the
   frame it first appeared on varied more, and its transition effectively began
   part way through its own travel.

   transition: none is the load-bearing half. The resting rule carries
   `transition: visibility 0s linear .6s`, so turning visibility on without
   killing the transition DELAYS it 600ms and the prep frame paints nothing at
   all, which is the bug this class was written for in the first place. */
.widget.drawer-prep .share-sheet,
.widget.drawer-prep .sheet { visibility: visible; transition: none; }
.widget.drawer-prep .share-panel,
.widget.drawer-prep .sheet-panel { transition: none; }
.widget.drawer-prep .share-scrim { visibility: visible; transition: none; }
.share-panel {
  /* Content height, not full height, which is the other two drawers' shape but
     not this one's. Frame 2709:580630 is 350 by 166 and that 166 is its whole
     content: a 64 header over an 86 body. The old drawer needed the height
     because it carried artwork, a name, two labelled sections and an input; the
     frame's body is four buttons. Left full height it would have been a white
     field with a row of icons stranded at the top.

     .share-sheet is already justify-content: flex-end, so the panel lands on the
     bottom edge with no anchoring of its own. max-height keeps it inside a card
     shorter than it is, and the body scrolls in that case. */
  pointer-events: auto; width: 100%; height: auto; max-height: 100%;
  background-color: var(--true-white); color: var(--ihr-gray-500);
  border-radius: var(--rad-6) var(--rad-6) 0 0;
  display: flex; flex-direction: column; justify-content: flex-start;
  overflow: hidden;
  transform: translateY(100%);
  /* Entering and leaving are NOT the same curve in production. accomplice runs
     `slideInBottom 600ms` on the way in, which is the default `ease`, and
     `slideInBottom 600ms reverse ease-in` on the way out. This had `ease` both
     ways, so the exit crawled out of the card the way the entry crawls in. The
     rest state carries the LEAVING curve; the open state overrides it. */
  transition: transform .6s var(--drawer-out);
  /* Its own compositor layer, for the whole life of the element rather than
     only while it moves.

     A full card of text, artwork and an input sliding on a PERCENTAGE transform
     lands on sub-pixel offsets every frame, and without a layer the browser
     re-rasterises all of it each time, which shows up as the text shimmering
     rather than as dropped frames. The main thread was already clean: 43 frames
     in 700ms, mean 16.7, nothing over 17, so rAF timing was never going to
     catch this. Promoted, the panel is rasterised once and the compositor just
     moves the texture.

     Declared at rest, not on the open class. Putting it on the class would
     destroy the layer the moment the class came off, which is exactly when the
     close animation still needs it. It costs nothing while the drawer is
     hidden, because a visibility:hidden subtree gets no layer either way. */
  will-change: transform;
}
.widget.share-open .share-panel { transform: translateY(0); transition: transform .6s var(--drawer-in); }
@media (prefers-reduced-motion: reduce) {
  .share-scrim, .share-sheet, .share-panel { transition: none; }
}
/* 64, not the 56 this was.

   The 56 came from accomplice's Drawer headingStyles, which is a plain height
   5.6rem bar. Frame 2709:580630 uses the newer header instead, 16 of padding
   around a 32 row, so 64, and that is already what the episode and info drawers
   on this card do. The share drawer was the odd one out.

   Two values follow the component rather than the frame, both the same calls
   already made on .sheet-head. The frame top-aligns the title against the close
   button, which sits it 4px above centre; centred is what every other drawer
   header does and what accomplice does. And the tracking is -0.5, the value the
   component carries, where the frame's H5 type style says 0. */
.share-head {
  display: flex; align-items: center; gap: var(--sp-16);
  flex: none; width: 100%; overflow: hidden; margin: 0;
  padding: var(--sp-16); box-sizing: border-box;
  background-color: var(--ihr-gray-200); color: var(--ihr-grey-600);
}
.share-head h2 {
  flex: 1 1 auto; min-width: 0; margin: 0;
  font-size: var(--fs-18); line-height: var(--lh-24); font-weight: 700;
  letter-spacing: var(--ls-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.share-close {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  flex: 0 0 auto; padding: var(--sp-4); aspect-ratio: 1 / 1;
  border-radius: var(--rad-999);
  display: inline-flex; align-items: center; justify-content: center;
}
.share-close img { width: 24px; height: 24px; display: block; }
.share-close:hover, .share-close:active { background-color: var(--ihr-gray-300); }
.share-close:focus-visible { outline: var(--border-2) solid var(--ihr-blue-600); outline-offset: -2px; }

/* The frame's Drawer Body, 16 of padding and one row. The gap is kept for the
   column that now has a single child, so a second section costs nothing later.
   The artwork, the name and description, and the two "Share on" and "Embed
   widget" section labels are all gone: the frame draws none of them. */
.share-body {
  overflow: auto; padding: var(--sp-16); display: flex; flex-direction: column;
  gap: var(--sp-16);
}
/* Icons Buttons, 2709:580663. */
.share-targets { display: flex; flex-direction: row; gap: var(--sp-8); }
/* Button Group, 2709:580664. A 48 circle over a 10px label, 8 between them. */
.share-target {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--sp-8);
  font-family: inherit; color: var(--ihr-grey-600);
  font-size: var(--fs-10); line-height: var(--lh-14); font-weight: 400;
  letter-spacing: var(--ls-4); padding: 0; text-decoration: none;
  flex: 0 0 auto; white-space: nowrap;
}
/* Button Icon, white with elevation1, 4 of padding around a 40 glyph. */
.share-target span.ring {
  width: 48px; height: 48px; padding: var(--sp-4); box-sizing: border-box;
  border-radius: var(--rad-999); background-color: var(--true-white);
  box-shadow: var(--elevation1);
  display: flex; align-items: center; justify-content: center;
}
/* 40, all four. It used to be 32 with Copy link alone at 40, which made that
   one target read as a size larger than its neighbours. The frame draws every
   Button Icon at 40. */
.share-target svg { width: 40px; height: 40px; display: block; }
.share-target:hover span.ring { background-color: var(--ihr-grey-150); }
.share-target:focus-visible span.ring { outline: var(--border-2) solid var(--ihr-blue-600); outline-offset: 2px; }

/* ---------- accomplice Menu, the playback speed list ----------
   From components/menu/menu.css.ts, and the item set from
   apps/listen/app/playback/actions/playback-speed.tsx, which opens exactly this
   menu from exactly this button.

     content   lightDark(brandWhite, gray600) surface, radius[2], elevation4,
               min-width 15.6rem, max-height 50vh, scrolls if it must
     item      14 / 18 / 400 at letterSpacing[1], space[8] padding, 4rem cap
     hover     lightDark(gray250, gray450)
     pressed   lightDark(gray200, gray500)
     motion    400ms ease, sliding in from 1rem on the opening side

   Every pair takes its LIGHT half. The cards are dark surfaces, which is why
   this first went in on the dark half, but the menu is a surface of its own
   sitting on top of the card rather than part of it, and iheart.com opens the
   light one. The menu opens UPWARD: this button sits low in every design, and
   the card clips its own overflow, so downward would open into nothing. */
/* The shell both menus share. Only the things that are the same in the code and
   in the Figma component live here; what the two sources disagree about is set
   per menu below. */
.ihr-menu {
  position: absolute; z-index: 12;
  max-width: 100%; max-height: 50vh; overflow: hidden auto;
  background-color: var(--true-white); color: var(--ihr-grey-450);
  scrollbar-color: initial var(--ihr-grey-200);
  box-shadow: 0 3px 5px rgba(0, 0, 0, .15);
  outline: none; padding: 0;
  animation: ihr-menu-in 400ms ease;
}
@keyframes ihr-menu-in { from { opacity: 0; transform: translateY(1rem); } }
@media (prefers-reduced-motion: reduce) { .ihr-menu { animation: none; } }
.ihr-menu button {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  display: flex; flex-direction: row; align-items: center; width: 100%;
  max-width: 100%; max-height: 4rem; gap: var(--sp-4);
  color: var(--ihr-grey-600); font-family: inherit; font-weight: 400;
  font-size: var(--fs-14); line-height: var(--lh-18); letter-spacing: var(--ls-1);
  text-align: left; user-select: none; overflow: hidden;
}
/* The leading glyph, accomplice's ShareMenuItem: <Share size={18} /> before the
   text, at the row's own colour. A row without one is not indented to match;
   production mixes the two in the same menu and lets the text start where it
   starts. */
.ihr-menu .mi-icon { flex: 0 0 auto; display: flex; align-items: center; }
.ihr-menu .mi-icon svg { width: 18px; height: 18px; display: block; }
.ihr-menu .mi-label { min-width: 0; overflow: hidden; text-overflow: ellipsis;
                      white-space: nowrap; }
.ihr-menu button:hover, .ihr-menu button:focus-visible,
.ihr-menu button[aria-checked="true"] { background-color: var(--ihr-grey-250); }
.ihr-menu button:active { background-color: var(--ihr-grey-200); }
.ihr-menu button:focus-visible { outline: none; }

/* The speed list, from menu.css.ts: radius[2] and space[8] padding. */
.speed-menu { min-width: 156px; border-radius: var(--rad-2); }
.speed-menu button { gap: var(--sp-8); padding: var(--sp-8); }

/* The episode row overflow, from the Menu instance in frames 2609:36099 and
   2609:37074: 206 wide, 6px radius, rows at 8 across and 12 down, which is why
   its two rows measure 84 tall. Those last two differ from what menu.css.ts
   says, and the frame is what this was asked against, so the frame wins here
   and the speed list above keeps the code's values. */
/* The share menu is the same Menu component at the same size, so it takes the
   same two lines rather than its own copy of them. */
.row-menu, .share-menu { min-width: 206px; border-radius: var(--rad-6); }
.row-menu button, .share-menu button { padding: 12px var(--sp-8); border-radius: var(--rad-2); }

/* ---------------------------------------------------------------------------
   Pause veil, frame 2581:363615.

   The prompt that covers the card once listening stops. It is drawn over the
   whole card rather than as a drawer at the bottom, so it is built like the
   scrim rather than like the sheets: inset 0 on .widget, which is the element
   that clips, and a flat 85% black instead of the drawer scrim's 80% plus
   blur. The frame has no blur behind it.

   It is armed on a pause rather than shown with one, so the transition is a
   plain fade with nothing to slide. Visibility is what makes it inert while
   hidden; opacity alone would leave the CTA clickable over the controls.
   --------------------------------------------------------------------------- */
.pause-veil {
  position: absolute; inset: 0; z-index: 22;
  display: flex; flex-direction: column;
  background: rgba(0, 0, 0, .85);
  overflow: hidden;
  opacity: 0; visibility: hidden;
  transition: opacity .3s ease, visibility 0s linear .3s;
}
.widget.veil-open .pause-veil {
  opacity: 1; visibility: visible;
  transition: opacity .3s ease, visibility 0s;
}
@media (prefers-reduced-motion: reduce) { .pause-veil { transition: none; } }

/* Drawer Body 2581:363725. Fills the veil and centres its one group. */
.pv-body {
  flex: 1 0 0; min-height: 0; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-8); padding: 16px; width: 100%; box-sizing: border-box;
}
.pv-group {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-8); width: 100%;
}
/* Logotype Secondary, dark theme, at the component's own 107x24 box. */
.pv-logo { display: block; width: 107px; height: 24px; flex: none; }

/* Digital Brand H5. Bold 18/24, centred, breaking inside a word rather than
   overflowing, which is what the frame's word-break carries. */
.pv-copy {
  margin: 0; width: 100%; text-align: center;
  font-family: inherit; font-weight: 700;
  font-size: var(--fs-18); line-height: var(--lh-24); letter-spacing: 0;
  color: var(--true-white); word-break: break-word;
}

/* Digital Brand Button 1 on the primary pill. Sized by its padding, not by a
   height, so the label sets the box exactly as the frame does. */
.pv-cta {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-4);
  padding: 12px 24px; border: 0; border-radius: 99px;
  background: var(--ihr-red-550); color: var(--true-white);
  box-shadow: 0 1px 1.5px rgba(0, 0, 0, .15);
  font-family: inherit; font-weight: 600;
  font-size: var(--fs-16); line-height: var(--lh-24); letter-spacing: var(--ls-0);
  text-decoration: none; white-space: nowrap; cursor: pointer;
  transition: background-color .2s ease, transform .2s ease;
}
/* Coloured buttons in accomplice hover only where there is a real pointer, and
   share their pressed colour with hover, the two told apart by the scale. */
@media (pointer: fine) {
  .pv-cta:hover { background-color: var(--ihr-red-400); }
}
.pv-cta:active { background-color: var(--ihr-red-400); transform: scale(.95); }
.pv-cta:focus-visible { outline: 2px solid var(--true-white); outline-offset: 2px; }

/* Header 2581:364648, inset by the body's own 16px padding.

   The frame's header reports x 32..350 with the 32px button flush to its right
   end, which reads as a button jammed into the card's corner. That is a stale
   placement, not the intent: the header is 318 wide, the width of a 350 card
   less 32, so it is inset 16 on the left and 0 on the right, and nothing else
   in the frame is asymmetric like that. The content group directly below it
   sits at 16..334. Every other drawer in this file puts its close inside a
   16px padded header, giving the same 16px inset. So the button lines up with
   the group's right edge rather than with the card's. */
.pv-close {
  position: absolute; top: 16px; right: 16px; z-index: 1;
  appearance: none; border: 0; background: transparent; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-4); border-radius: var(--rad-999);
  transition: background-color .2s ease, transform .2s ease;
}
.pv-close img { display: block; width: 24px; height: 24px; }
.pv-close:hover, .pv-close:active { background-color: var(--ihr-gray-400); }
.pv-close:active { transform: scale(.95); }
.pv-close:focus-visible { outline: 2px solid var(--true-white); outline-offset: -1px; }

/* Below 320 the sentence needs a third line, which takes the group to 160 and
   overflows the shortest card in the set by 12: the bar card's live variant,
   which is 180 tall because it carries no episode list. Measured across
   280/320/360/420/560/760 on all six cards, and that is the only combination
   that fails, so the step is keyed to where the wrap happens rather than
   applied by design.
   The close button is unaffected: it resolves against the body's padding box,
   which the padding change does not move. */
/* The actions go when the row is too narrow to hold them AND readable text.
   They cost a flat 64px at every width, which at a 240px card is a quarter of it
   and leaves the title 16 characters; without them it gets 28.

   This is the same rule the card already applies below 128 tall, on the axis
   that also runs out. 319 is reused rather than a new number: it is already the
   width breakpoint below. */
/* The top bar artwork goes below 201 wide, at every height. From the
   breakpoints matrix in Figma, section 2719:602052.

   It is a width rule and only a width rule. It was briefly a two-axis rule,
   keeping the tile at 51 to 200 wide once the card cleared 128 tall, on the
   reasoning that above 128 the play button leaves the top bar and stops
   competing for the row. That was tried and reversed: the tile still costs the
   title 44px there, and at 200 wide the title is the thing worth protecting.
   Readability won over showing the artwork.

   The tile stops shrinking at 252, where --thumb reaches its 36px floor, and
   held that 36 all the way down to 50, so below 252 it was a square that had
   given up adapting while still taking 8 of gap and 36 of row from the title.
   Measured at 200 wide, dropping it hands the title 44px back, about five more
   characters before it truncates.

   201 as drawn, not 252 where the floor is. Those were the two candidates and
   the Figma line is the one chosen.

   Live radio is NOT carved out, even though its tile is a station logo rather
   than an episode cover and is the most identifying thing on that card. It does
   not need to be: the card's backdrop is already that logo at full bleed, so at
   190 wide it still reads as Z100 with the tile gone. Checked rather than
   assumed. */
@container (max-width: 200px) {
  .widget.c .topbar a.thumb-link { display: none; }
}

/* The scrubber, from the same matrix. Two conditions, either one hides it.

     below 201 wide     the bar is not scrubbable at that size and the two
                        timestamps alone take about 80px out of a 174px row.
                        Measured at 190 wide, the track between them is 60px.
     100 tall or less   the whole bottom bar is gone in that row in Figma.

   Scoped to .widget.c. The hero card shares this class and keeps its own
   scrubber at every size. */
@container (max-width: 200px) {
  .widget.c .slider { display: none; }
}
@container (max-height: 100px) {
  .widget.c .slider { display: none; }
}

/* The brand lockup goes at 50 wide. The matrix draws it in every cell from 51
   up and in none of the 50 column. Its own content is a 53px "Listen on" beside
   a 62px logo, so 115 before any padding, and a 50px card cannot hold a third of
   it. Height is handled above, where the whole bottom bar goes at 100 and below. */
@container (max-width: 50px) {
  .widget.c .list-row { display: none; }
}

/* The playback speed button goes below 241 wide, from the breakpoints matrix in
   Figma, section 2719:602052. Its playback row holds back 15 and forward 30 at
   51 to 240, and gains speed at 241, which is why the secondary count in the
   annotations reads 2 and then 4.

   Speed is the right one of the three to drop. Back 15 and forward 30 are the
   only way to move within an episode from this card, where speed is a
   preference that survives in the full player, so losing it costs the least.

   Scoped to the transport row, not the top bar. The 1x control is a text box
   rather than a glyph and carries its own sizing, so hiding the button hides
   both. */
@container (max-width: 240px) {
  .widget.c .hero-controls .h-btn[data-act="speed"] { display: none; }
}

/* The title and subtitle go at 50 wide. The matrix draws them from 51 up and
   nowhere in the 50 column. At 50 the card is 8 of padding either side and a 40
   play button, so there is no room left for a line of text to be read in. */
/* The LIST ROWS shed their own furniture, which is a different rule from the
   player shedding its. From the breakpoints matrix in Figma, section
   2719:602052, read off the annotations on the List rather than off the
   inventory line.

     row artwork        gone at 240 wide and below
     row overflow       gone at 200 wide and below

   Measured at 200 wide, a row was carrying a 56px tile, a 32px button and two
   12px gaps, which left the episode title about 88px and collapsed it to one
   character and an ellipsis. Dropping both hands the title the whole row.

   Applied to the drawer rows as well as the inline ones. The matrix only draws
   the inline list, but the drawer is the same rows at the same card width and
   has the same squeeze, so splitting them would mean a 200px card whose drawer
   is legible and whose inline list is not. */
@container (max-width: 240px) {
  .widget.c .row .row-art { display: none; }
}
@container (max-width: 200px) {
  .widget.c .row-wrap .row-more { display: none; }
}

/* The play button is 34 at 50 wide, from the breakpoints matrix in Figma, which
   draws it at 34 in every cell of its 50 column.

   It overrides the 40px floor in --play. That floor is not arbitrary: it came
   from the older compact frame 2666:125692, which draws the button at 40 and was
   read as the smallest it should ever be. The matrix supersedes it at this one
   width. Everywhere else the 40 still holds. */
@container (max-width: 50px) {
  /* .shell[data-fill] is carried because the base --play rule has it, and a
     container query adds no specificity of its own. Without it this loses and
     the button stays on its 40 floor. */
  .shell[data-fill] .widget.c .hero-play { --play: 34px; }
  .widget.c .topbar .meta { display: none; }
  /* The skip controls go too. The matrix draws no secondary controls anywhere
     in its 50 column, and a 50px card is 8 of padding either side and a 40 play
     button, so there is nothing left for them to sit in. Hidden on .cc-side
     rather than on the buttons, so both groups go together. */
  .widget.c .hero-controls .cc-side { display: none; }
}

/* The trailing buttons arrive one at a time as the card widens, from the
   breakpoints matrix in Figma, section 2719:602052.

   This replaces a single cliff. Everything used to appear at once at 320 and
   nothing existed below it, which is why the matrix's column edges sat where
   they did and why a 310px card and a 330px card behaved so differently.

                         share   list   info
     128 tall and up      201     241    341
     127 and below        241     301    341

   The short rows need more width for the same buttons because the play button
   has moved into this row and is competing for it.

   POSITIONAL, not per button. The rule counts from the right, so the far right
   slot survives longest and each content type fills from that end with whatever
   buttons it has. Podcast and artist radio carry list and share, episode and
   live carry info and share, playlist carries all three, and none of that needs
   its own rule. The markup order is already info, list, share, which is Figma's
   s1, s2, s3.

   :nth-last-of-type counts BUTTONS only. The top bar's other children are an <a>
   and a <div>, which are different element types and so are not counted, where
   :nth-last-child would have counted them and shifted everything by two. */
@container (max-width: 200px) {
  .widget.c .topbar button.h-btn:nth-last-of-type(1) { display: none; }
}
@container (max-width: 240px) {
  .widget.c .topbar button.h-btn:nth-last-of-type(2) { display: none; }
}
@container (max-width: 340px) {
  .widget.c .topbar button.h-btn:nth-last-of-type(3) { display: none; }
}
@container (max-width: 240px) and (max-height: 127px) {
  .widget.c .topbar button.h-btn:nth-last-of-type(1) { display: none; }
}
@container (max-width: 300px) and (max-height: 127px) {
  .widget.c .topbar button.h-btn:nth-last-of-type(2) { display: none; }
}

@container (max-width: 319px) {
  .pv-body { padding: 8px 16px; }
  .pv-copy { font-size: var(--fs-14); line-height: var(--lh-18); }
  .pv-cta { padding: 8px 20px; font-size: var(--fs-14); line-height: var(--lh-18); }
}


/* ===========================================================================
   Unsupported region, recreated.

   The one drawing on a page of live embeds, because the state is gated on the
   viewer's country and the request cannot be moved outside the US. Rebuilt from
   the shipping widget's source, not from a screenshot, so every number below is
   the real one rather than an eyeballed match.

   The widget's root font size is 10px, so its rem values halve neatly into
   pixels here. 1rem is 10px, 11.6rem is the 116px thumb, 1.7rem is the 17px
   button radius. Colours are the LIGHT theme, which is what the embed serves:
   fontPrimary #27292D, fontTertiary #3F4447, modalBackground white, and the
   thumb shadow is gray 300. Checked against the live 404 frame on this same
   page, whose heading measured rgb(39, 41, 45) in Helvetica Neue at a 10px
   root, which is the same chrome these states are built on.
   ==========================================================================*/
.georecreate { display: flex; flex-direction: column; gap: 6px; }
/* The first version of this section labelled the panels with the names of the
   code constants, UnsupportedCountry and AccessDenied, which read as variant
   names someone is being asked to choose between. They are not options, they
   are four states that all ship. The banner and the per panel chip say so
   plainly, and the constant names moved down into the captions where they
   belong as detail rather than as a heading. */
.ships-today { font-size: 13px; line-height: 1.5; margin: 6px 0 2px;
               padding: 10px 12px; border-radius: 6px;
               background: var(--ihr-grey-100, #F6F8F9);
               border-left: 3px solid var(--ihr-red-500, #C6002B);
               color: var(--ihr-grey-600, #27292D); }
.geo-chip { display: inline-block; vertical-align: 1px; margin-right: 6px;
            padding: 2px 7px; border-radius: 10px; font-size: 10px;
            font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
            background: var(--ihr-grey-600, #27292D); color: #fff; }
.geo-label { font-size: 12px; font-weight: 600; color: var(--ihr-grey-450);
             margin-top: 10px; }
/* 300 tall to sit at the same height as the embeds above, and white because the
   embed is served on a transparent body over the host's page. */
.geo-panel {
  position: relative; height: 300px; box-sizing: border-box;
  background: #fff; color: #27292D; text-align: center; padding: 20px;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  border: 1px solid var(--ihr-grey-200, #E6EAED); border-radius: 6px;
  overflow: hidden;
}
.geo-ihr { position: absolute; right: 10px; top: 10px; width: 20px; height: 16px; }
/* Centred only from 169 tall up, which is the widget's own condition. */
.geo-mid { display: flex; align-items: center; justify-content: center; height: 100%; }
.geo-thumb { display: block; width: 116px; height: 116px; margin: 10px auto;
             border-radius: 6px; background: #fff;
             box-shadow: #C5CDD2 2px 3px 10px 0px; object-fit: contain; }
.geo-h1 { font-size: 20px; line-height: 26px; font-weight: bold;
          margin: 0 0 20px; color: #27292D; }
.geo-body { font-size: 14px; line-height: 18px; font-weight: normal;
            margin: 0 0 20px; color: #27292D; }
/* ButtonsWrapper puts 1rem between siblings and 1rem under the group. */
.geo-btns { margin-bottom: 10px; }
.geo-btns > * + * { margin-left: 10px; }
/* RoundedButton at 420 wide and up, which every card on this page clears. */
.geo-btn { display: inline-block; box-sizing: border-box; min-width: 110px;
           height: 32px; line-height: 20px; padding: 5px 14px; font-size: 14px;
           border-radius: 17px; border: 1px solid #27292D; text-align: center; }
.geo-btn-out  { background: transparent; color: #27292D; }
/* ReloadButton inverts: fontPrimary fill, backgroundPrimary text and border. */
.geo-btn-fill { background: #27292D; color: #fff; border-color: #fff; }
.geo-textlink { display: inline-block; font-size: 12px; line-height: 1.4;
                color: #27292D; text-decoration: underline; }
/* The artist and playlist panel is NOT a scrim overlay. It is inset 0, opaque
   and z-index 9999, so the player is gone rather than dimmed. 1.4rem padding
   and the close button floats right. */
.geo-modal { text-align: left; padding: 14px; }
.geo-close { float: right; font-size: 20px; line-height: 1; color: #27292D; }
.geo-title { font-size: 16px; line-height: 20px; margin: 0;
             padding-bottom: 10px; padding-right: 30px; color: #27292D; }
.geo-mbody { font-size: 16px; line-height: 20px; color: #3F4447; }
.geo-mbody p { margin: 0; }
/* The thumb is dropped under 300 wide in the widget itself. */
@media (max-width: 299px) { .geo-thumb { display: none; } }
