/* The Homeowners Show: site stylesheet.
   Token layer is imported verbatim from the design system (do not edit the
   files under css/tokens/). Everything below the import block is the port of
   the design's component styles into plain CSS classes. */

@import url("tokens/fonts.css");
@import url("tokens/colors.css");
@import url("tokens/typography.css");
@import url("tokens/spacing.css");
@import url("tokens/shape.css");
@import url("tokens/motion.css");
@import url("tokens/base.css");

/* ==========================================================================
   Page harness: ported from the design file's <style> block
   ========================================================================== */

body {
  margin: 0;
  background: var(--surface-page);
  font-family: var(--font-body);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

main {
  flex: 1;
}

a {
  color: var(--accent-press);
  text-decoration: none;
}

a:hover {
  color: var(--ink-900);
}

.hos-visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Inline icon wrapper: mirrors the design system's <Icon> span. */
.hos-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  color: currentColor;
}

.hos-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* ==========================================================================
   Button: ported from components/core/Button.jsx
   ========================================================================== */

.hos-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-body);
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: var(--radius-md);
  border: var(--border-2) solid transparent;
  cursor: pointer;
  width: auto;
  white-space: nowrap;
  padding-block: 0;
  min-height: var(--tap-min);
  transition: var(--transition-interactive);
}

.hos-btn:active {
  transform: scale(var(--press-scale));
}

.hos-btn--sm { height: 40px; padding-inline: 16px; font-size: var(--text-xs); }
.hos-btn--md { height: 48px; padding-inline: 22px; font-size: var(--text-sm); }
.hos-btn--lg { height: 58px; padding-inline: 30px; font-size: var(--text-base); }

.hos-btn--full {
  width: 100%;
  height: auto;
  white-space: normal;
  padding-block: 10px;
}

.hos-btn--full.hos-btn--sm { min-height: 40px; }
.hos-btn--full.hos-btn--md { min-height: 48px; }
.hos-btn--full.hos-btn--lg { min-height: 58px; }

.hos-btn--primary {
  background: var(--accent);
  color: var(--text-on-accent);
  box-shadow: none;
}

.hos-btn--primary:hover { background: var(--accent-hover); color: var(--text-on-accent); }
.hos-btn--primary:active { box-shadow: var(--shadow-accent-press); }

.hos-btn--secondary {
  background: transparent;
  color: var(--ink-900);
  border-color: var(--ink-900);
}

.hos-btn--secondary:hover { background: var(--ink-900); color: var(--paper); }

.hos-btn--ghost {
  background: transparent;
  color: var(--paper);
  border-color: rgba(255, 255, 255, .38);
}

.hos-btn--ghost:hover { background: var(--white); color: var(--ink-900); border-color: var(--white); }

.hos-btn--quiet {
  background: transparent;
  color: var(--text-body);
  border-color: transparent;
}

.hos-btn--quiet:hover { background: var(--surface-sunken); color: var(--text-body); }

.hos-btn--sm .hos-icon,
.hos-btn--md .hos-icon { width: 18px; height: 18px; }
.hos-btn--lg .hos-icon { width: 20px; height: 20px; }

/* Phone: long labels wrap instead of pushing the page into horizontal scroll.
   The design system button is nowrap at every width, which overflows a 390px
   viewport on the longest label ("Listen to the latest episode"). */
@media (max-width: 640px) {
  .hos-btn {
    max-width: 100%;
    white-space: normal;
    height: auto;
    padding-block: 10px;
    text-align: center;
  }

  .hos-btn--sm { min-height: 40px; }
  .hos-btn--md { min-height: 48px; }
  .hos-btn--lg { min-height: 58px; padding-inline: 22px; }
}

/* ==========================================================================
   IconButton: ported from components/core/IconButton.jsx
   ========================================================================== */

.hos-icon-btn {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: var(--transition-interactive);
  background: transparent;
  color: var(--ink-900);
  border: var(--border-1) solid var(--border-hairline);
  padding: 0;
}

.hos-icon-btn:hover { background: var(--ink-900); color: var(--paper); }
.hos-icon-btn .hos-icon { width: 20px; height: 20px; }

.hos-icon-btn--on-dark {
  background: transparent;
  color: var(--paper);
  border: var(--border-1) solid rgba(255, 255, 255, .22);
}

.hos-icon-btn--on-dark:hover { background: rgba(255, 255, 255, .12); color: var(--accent); }

/* ==========================================================================
   PlayButton: ported from components/core/PlayButton.jsx

   The JSX sizes the icon off the button (icon = size * .42) and nudges the
   play triangle right by size * .03 so it reads optically centred; the pause
   glyph gets no nudge. Both fall out of the --play-size custom property, so a
   size variant only has to set one value. Default 56 matches the component
   default; the mini player uses 44 and the hero player 72-88.
   ========================================================================== */

.hos-play-btn {
  --play-size: 56px;
  width: var(--play-size);
  height: var(--play-size);
  min-width: var(--play-size);
  padding: 0;
  border: none;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: var(--text-on-accent);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  box-shadow: var(--shadow-sm);
  transition: var(--transition-interactive);
}

.hos-play-btn:hover { background: var(--accent-hover); }

.hos-play-btn:active {
  box-shadow: var(--shadow-accent-press);
  transform: scale(var(--press-scale));
}

.hos-play-btn:focus-visible {
  outline: none;
  box-shadow: var(--ring-focus);
}

.hos-play-btn .hos-icon {
  width: calc(var(--play-size) * .42);
  height: calc(var(--play-size) * .42);
  margin-left: calc(var(--play-size) * .03);
}

.hos-play-btn[data-playing="true"] .hos-icon { margin-left: 0; }

.hos-play-btn--sm { --play-size: 44px; }
.hos-play-btn--md { --play-size: 48px; }  /* EpisodeCard: PlayButton size={48} */
.hos-play-btn--lg { --play-size: 72px; }
.hos-play-btn--xl { --play-size: 88px; }

/* ==========================================================================
   Mini player: ported from the design file's nowPlaying sticky bar

   #mini-player is the last flex child of <body>, so position:sticky pins it to
   the bottom of the viewport for the whole scroll and lets it settle at the
   end of the document, exactly as in the design. The <audio> element itself
   lives outside this markup (js/player.js owns it) so hiding the bar can never
   tear down the element that is playing.
   ========================================================================== */

/* THE SAFE-AREA PAD IS WHAT KEEPS THE TRANSPORT OFF THE HOME INDICATOR.
   On a notched iPhone the bottom ~34pt of the screen belongs to the system's
   home-indicator gesture area. A bar pinned to `bottom: 0` draws underneath
   it, so the play/pause and skip buttons sat in a strip where a swipe goes to
   iOS instead of to the page - the controls were visible and not reliably
   tappable. env(safe-area-inset-bottom) is that inset, and 0 on every device
   that does not have one, so the fallback in env()'s second argument covers
   browsers that do not know the function at all and the bar is unchanged on a
   phone with square corners or on the desktop.

   It pads rather than shifts: the background and the blur still run to the
   physical bottom of the screen, which is what makes the bar look anchored,
   and only the CONTENT moves up out of the gesture strip.

   viewport-fit=cover has to be on the <meta name="viewport"> for env() to
   report anything but 0 - see partials/head.php, where it is set for this. */
.hos-mini-player {
  position: sticky;
  bottom: 0;
  z-index: 70;
  background: rgba(14, 14, 15, .96);
  backdrop-filter: var(--blur-glass);
  -webkit-backdrop-filter: var(--blur-glass);
  border-top: var(--border-1) solid var(--border-inverse);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

.hos-mini-player[hidden] { display: none; }

.hos-mini-player__bar {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  height: 76px;
}

.hos-mini-player__art {
  width: 48px;
  height: 48px;
  flex: 0 0 auto;
  border-radius: var(--radius-sm);
  object-fit: contain;
  padding: 4px;
  background: var(--white);
  box-sizing: border-box;
}

/* Same contain-vs-cover switch as .hos-card-art--thumb, for the one image
   js/chrome.js still draws itself. Added by miniPlayerMarkup() only when the
   playing episode's slug carried a data-thumb, and removed again by the img's
   onerror.

   THIS ONE STAYS SQUARE, unlike every other thumbnail box on the site. The
   16:9 reshape over in .hos-card-art--thumb exists to stop the side crop eating
   the headline off a title card; at 48x48 there is no headline to save - the
   card's text is illegible at that size whatever the crop - and 16:9 would make
   it 48x27, a letterbox slot inside a 76px bar, throwing the transport row's
   vertical rhythm off to protect text nobody can read. The cost is the two
   letterbox bars surviving as about 6px of black top and bottom. That is the
   right trade at this size: it is an identifier, not a picture. */
.hos-mini-player__art--thumb {
  object-fit: cover;
  padding: 0;
}

/* display:block is load-bearing since the art and the text became one expand
   button: both this column and the two lines inside it are <span>s now (a
   <div> is not valid inside a <button>), and text-overflow:ellipsis does
   nothing to an inline box. */
.hos-mini-player__meta {
  display: block;
  min-width: 0;
  flex: 1;
}

/* Truncate rather than wrap. The bar is a fixed height and the text column is
   the only elastic child, so once the transport grew to four controls this
   label had barely 100px on a phone - enough to wrap "Now playing · Ep 357"
   onto three lines and push the title out of the bar. It is a label, not
   content: clipping it is always better than reflowing the bar. */
.hos-mini-player__eyebrow {
  display: block;
  color: var(--accent);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.hos-mini-player__title {
  display: block;
  color: var(--white);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Phone: the art is the first thing to go, the title keeps the space. The gap
   tightens too - four 44px controls plus four gaps is most of a 375px screen,
   and every pixel saved here goes to the title. The skip buttons deliberately
   stay: a phone is where skipping past an ad matters most, and at 375px the
   row still fits with no overflow (the title just ellipsises earlier). */
@media (max-width: 480px) {
  .hos-mini-player__bar { gap: var(--space-2); height: 68px; }
  .hos-mini-player__art { display: none; }
}

/* THE WAY INTO THE FULL PLAYER. The art and the text column are one button
   (js/chrome.js's miniPlayerMarkup), so the expand affordance is reachable by
   keyboard and not only by a tap on a div. It is dressed as the row it
   replaced: no border, no background, the same flex behaviour the two children
   had when they were siblings, and the focus ring is the only thing that ever
   makes it look like a control. */
.hos-mini-player__expand {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex: 1;
  min-width: 0;
  padding: var(--space-1);
  margin: calc(var(--space-1) * -1);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: var(--transition-interactive);
}

.hos-mini-player__expand:hover { background: rgba(255, 255, 255, .07); }

@media (max-width: 480px) {
  .hos-mini-player__expand { gap: var(--space-2); }
}

/* ==========================================================================
   Now playing: the full-screen sheet the mini player expands into
   (js/nowplaying.js)

   TWO SHAPES, ONE COMPONENT, and the owner's own framing for why: the pattern
   is "typical on mobile, doesn't translate exactly on desktop". Under 901px
   this is the whole screen and it swipes away. From 901px it is a 420px card
   docked above the bar it came from, dismissed by the chevron, Escape or a
   click outside - no swipe, because a mouse has no such gesture and inventing
   one for it is how a desktop UI ends up feeling like a phone port.

   THE CLOSED STATE IS visibility, NOT [hidden]. Both take it out of the tab
   order and out of the accessibility tree; only visibility can be transitioned,
   and transitioning it with a delay is what lets the panel finish sliding out
   before the sheet stops existing. display:none would cut the exit off at frame
   one. `inert` is set alongside it by the script, so the closed sheet is
   unreachable even where visibility is overridden by a user stylesheet.
   ========================================================================== */

.hos-nowplaying {
  position: fixed;
  inset: 0;
  z-index: 80;   /* over the mini player (70) and the transcript escape (65) */
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
}

.hos-nowplaying[data-state="closed"] {
  visibility: hidden;
  pointer-events: none;
  transition: visibility 0s linear var(--dur-base);
}

.hos-nowplaying__scrim {
  position: absolute;
  inset: 0;
  background: var(--overlay-scrim);
  opacity: 1;
  transition: opacity var(--dur-base) var(--ease-standard);
}

.hos-nowplaying[data-state="closed"] .hos-nowplaying__scrim { opacity: 0; }

.hos-nowplaying__panel {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--ink-900);
  color: var(--paper);
  transition: transform var(--dur-base) var(--ease-out);
  will-change: transform;
}

/* While a finger is on the handle the panel is under the finger, not easing
   towards it. The script sets this for the length of the gesture only. */
.hos-nowplaying[data-dragging="true"] .hos-nowplaying__panel { transition: none; }

.hos-nowplaying__grip {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 28px;
  cursor: grab;
  /* The handle IS the gesture surface, so the browser must not claim the
     vertical drag for a scroll before the handler sees it. */
  touch-action: none;
}

.hos-nowplaying__grip-bar {
  width: 44px;
  height: 5px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .32);
}

.hos-nowplaying__head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 0 var(--gutter);
  touch-action: none;
}

.hos-nowplaying__minimise {
  width: 44px;
  height: 44px;
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--paper);
  cursor: pointer;
  transition: var(--transition-interactive);
}

.hos-nowplaying__minimise:hover { background: rgba(255, 255, 255, .12); color: var(--accent); }
.hos-nowplaying__minimise .hos-icon { width: 26px; height: 26px; }

.hos-nowplaying__head-label {
  font-family: var(--font-body);
  font-size: var(--text-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gray-400);
}

.hos-nowplaying__stage {
  flex: 0 0 auto;
  padding: var(--space-4) var(--gutter) 0;
  touch-action: none;
}

/* THE ART BOX IS 16:9, NOT SQUARE, and that is the opposite of what a phone
   podcast player usually does - for the reason set out at length on
   .hos-card-art--thumb below. This show's artwork is YouTube thumbnails, and
   hqdefault.jpg is a 16:9 frame letterboxed into 4:3 with a 45px black bar top
   and bottom. In a square box `cover` keeps those bars and throws away 12.5%
   off each SIDE, which on a title card takes the first word off the headline.
   At 16:9 the crop lands on the bars instead: zero horizontal loss, letterbox
   gone. The rest of the site made this exact trade for its cards; a square
   here would be the one place on the site showing the black bars back.

   The SHAPE is on the box and the FIT is on the image, so the img's onerror -
   which swaps a failed thumbnail for the logo badge and drops the --thumb
   class - changes only the fit and never resizes the sheet underneath. */
.hos-nowplaying__art-box {
  width: 100%;
  max-width: 340px;
  margin: 0 auto var(--space-6);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--white);
  box-shadow: var(--shadow-pop);
}

.hos-nowplaying__art {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 6% 10%;
  box-sizing: border-box;
}

/* The badge is a mark on white and wants contain plus that padding; a
   thumbnail is a photograph and wants to fill the frame. This class is that
   switch and nothing else - the same one .hos-card-art--thumb is. */
.hos-nowplaying__art--thumb {
  object-fit: cover;
  padding: 0;
}

.hos-nowplaying__eyebrow {
  margin: 0 0 var(--space-2);
  color: var(--accent);
}

.hos-nowplaying__eyebrow[hidden] { display: none; }

.hos-nowplaying__title {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--text-h4);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-display);
  color: var(--white);
}

/* -- the scrubber ---------------------------------------------------------
   A real <input type="range">, because everything a seek control needs from a
   keyboard and a screen reader is already in one. Only the track and the thumb
   are styleable, and both engines want their own pseudo-element for each, so
   the four rules below say the same two things twice. --hos-scrub is the played
   fraction, written by js/nowplaying.js on every repaint; it is what makes the
   track fill behind the thumb. */

.hos-nowplaying__scrub { padding: var(--space-5) var(--gutter) 0; }

.hos-nowplaying__range {
  --hos-scrub: 0%;
  display: block;
  width: 100%;
  height: 44px;   /* the TAP TARGET; the visible track is 6px inside it */
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}

.hos-nowplaying__range:disabled { cursor: default; opacity: .5; }

.hos-nowplaying__range::-webkit-slider-runnable-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: linear-gradient(to right,
    var(--accent) var(--hos-scrub), rgba(255, 255, 255, .22) var(--hos-scrub));
}

.hos-nowplaying__range::-moz-range-track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: linear-gradient(to right,
    var(--accent) var(--hos-scrub), rgba(255, 255, 255, .22) var(--hos-scrub));
}

.hos-nowplaying__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  margin-top: -6px;   /* centre the 18px thumb on the 6px track */
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--white);
  box-shadow: var(--shadow-sm);
}

.hos-nowplaying__range::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border: 0;
  border-radius: var(--radius-pill);
  background: var(--white);
  box-shadow: var(--shadow-sm);
}

.hos-nowplaying__range:focus-visible { outline: none; box-shadow: var(--ring-focus); border-radius: var(--radius-pill); }

.hos-nowplaying__times {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  color: var(--gray-400);
}

/* -- the transport -------------------------------------------------------- */

.hos-nowplaying__transport {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-8);
  padding: var(--space-5) var(--gutter);
}

/* 56px, not the icon button's usual 44. This is the primary transport of a
   full-screen player held in one hand; the mini bar's row is the compact one. */
.hos-nowplaying__skip {
  width: 56px;
  height: 56px;
  border-radius: var(--radius-pill);
}

.hos-nowplaying__skip .hos-icon { width: 26px; height: 26px; }

/* -- the audio output picker ---------------------------------------------- */

/* A ROW THAT DISAPPEARS WHEN IT IS EMPTY. js/nowplaying.js leaves this mount
   with no children on any engine that cannot offer a playback-target picker,
   and :empty is what turns "no button" into "no gap" without the script having
   to hide anything itself. */
.hos-nowplaying__aux {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--gutter) var(--space-5);
}

.hos-nowplaying__aux:empty { display: none; }

/* 44px, the site's ordinary icon-button target - not the transport's 56. This
   is a secondary control and sizing it like a primary one would make it read
   as a fourth transport button. */
.hos-nowplaying__output {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-pill);
}

.hos-nowplaying__output .hos-icon { width: 22px; height: 22px; }

/* Playing to something that is not this phone. Deliberately quiet: the accent
   and a faint wash, the same treatment the minimise control takes on hover, so
   it reads as "this is on" rather than as an alert. */
.hos-nowplaying__output[data-remote="true"] {
  color: var(--accent);
  background: rgba(255, 255, 255, .12);
  border-color: var(--accent);
}

/* The engine has told us there is nothing to send to. Dimmed, never removed:
   receivers appear and vanish as a phone moves around a house, and a control
   that disappeared under the thumb would be worse than one that looks idle. */
.hos-nowplaying__output[data-target="none"] { opacity: .55; }

/* -- the show notes ------------------------------------------------------- */

/* THE ONLY SCROLLING REGION IN THE SHEET, which is what makes the swipe
   unambiguous: everything above it drags the sheet, this scrolls its own
   content, and neither has to guess which the visitor meant.
   -webkit-overflow-scrolling keeps the momentum scroll iOS gives a document to
   a region that is not one. */
.hos-nowplaying__notes {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 0 var(--gutter) var(--space-8);
  border-top: var(--border-1) solid var(--border-inverse);
  padding-top: var(--space-5);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  line-height: var(--leading-long);
  color: var(--gray-200);
  overscroll-behavior: contain;
}

.hos-nowplaying__notes p { margin: 0 0 var(--space-4); }
.hos-nowplaying__notes p:last-child { margin-bottom: 0; }

.hos-nowplaying__notes-head {
  margin: 0 0 var(--space-3);
  font-family: var(--font-body);
  font-size: var(--text-label);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--gray-400);
}

.hos-nowplaying__notes-empty { color: var(--gray-500); }

/* -- PHONE: the whole screen ---------------------------------------------
   THE HEIGHT IS DECLARED TWICE, vh THEN dvh, for the reason the menu panel
   declares it twice: 100vh on a phone is the LARGE viewport, the one that
   assumes the browser's own bars are retracted, so a sheet sized to it puts
   its transport under the address bar until you scroll. 100dvh is the viewport
   as it is right now. A browser that does not know dvh keeps the vh line.

   The bottom pad is the home-indicator strip, the same env() inset the mini
   player takes - a 72px play button sitting in the system's gesture area is a
   play button that does not reliably respond. */
@media (max-width: 900px) {
  .hos-nowplaying__panel {
    height: 100vh;
    height: 100dvh;
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }

  .hos-nowplaying[data-state="closed"] .hos-nowplaying__panel {
    transform: translate3d(0, 100%, 0);
  }
}

/* -- DESKTOP: a card docked above the bar --------------------------------
   Not full screen, and deliberately not: a 27" display filled edge to edge
   with one episode's artwork is a phone gesture wearing a desktop's clothes.
   It sits above the mini player it came from, right-aligned to the same
   gutter, so the expand reads as the bar growing rather than as a new window.
   88px of clearance is the 76px bar plus a hair. */
@media (min-width: 901px) {
  .hos-nowplaying {
    justify-content: flex-end;
    align-items: flex-end;
    padding: 0 var(--gutter-desktop) 88px;
  }

  .hos-nowplaying__scrim { background: transparent; }

  .hos-nowplaying__panel {
    width: 420px;
    max-width: calc(100vw - var(--gutter-desktop) * 2);
    max-height: min(72vh, 720px);
    border: var(--border-1) solid var(--border-inverse);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-pop);
    overflow: hidden;
    transition: transform var(--dur-base) var(--ease-out),
                opacity var(--dur-base) var(--ease-standard);
  }

  /* No drag handle on a mouse: there is no gesture for it to hint at. */
  .hos-nowplaying__grip { display: none; }
  .hos-nowplaying__head { padding-top: var(--space-3); }
  .hos-nowplaying__art-box { max-width: 240px; margin-bottom: var(--space-4); }

  .hos-nowplaying[data-state="closed"] .hos-nowplaying__panel {
    transform: translate3d(0, var(--space-3), 0);
    opacity: 0;
  }
}

/* MOTION IS THE FIRST THING TO GO. The tokens already collapse every duration
   to 0ms under this query, which stops the slide and the fade on their own;
   what is left is the transform they were sliding, and a sheet that arrives
   already in place is the whole point. The visibility delay goes too, or the
   closed sheet would linger for a transition that no longer runs. */
@media (prefers-reduced-motion: reduce) {
  .hos-nowplaying[data-state="closed"] { transition: none; }
  .hos-nowplaying[data-state="closed"] .hos-nowplaying__panel { transform: none; }
  .hos-nowplaying__panel { transition: none; }
  .hos-nowplaying__scrim { transition: none; }
}

/* ==========================================================================
   SiteHeader: components/navigation/SiteHeader.jsx + the design file's
   site-context overrides (cream bar, 98px → 70px on scroll, 74px → 48px logo)
   ========================================================================== */

.hos-header {
  position: sticky;
  top: 0;
  z-index: 60;
  background: #F7F5F2;
  border-bottom: var(--border-1) solid var(--gray-200);
}

.hos-header__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  height: 98px;
  transition: height var(--dur-base) var(--ease-standard);
}

.hos-header__logo {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-decoration: none;
  /* The badge is a fixed asset, not a flexible one. Without this the logo is the
     flex item that gives when the row runs short, and it squeezes to nothing
     while the nav keeps its full gaps (Plan Amendment 1, section A: six items). */
  flex: 0 0 auto;
}

.hos-header__logo img {
  height: 74px;
  width: auto;
  transition: height var(--dur-base) var(--ease-standard);
}

html[data-scrolled="yes"] .hos-header__bar { height: 70px; }
html[data-scrolled="yes"] .hos-header__logo img { height: 48px; }
html[data-scrolled="yes"] .hos-menu { top: 70px; height: calc(100vh - 70px); height: calc(100dvh - 70px); }

.hos-header__nav {
  display: flex;
  align-items: center;
  gap: var(--space-8);
}

.hos-nav-links {
  display: flex;
  gap: var(--space-8);
  list-style: none;
  margin: 0;
  padding: 0;
}

.hos-nav-links a {
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-900);
  padding-bottom: 4px;
  border-bottom: var(--border-2) solid transparent;
  transition: var(--transition-interactive);
}

.hos-nav-links a:hover { color: var(--accent-press); }

.hos-nav-links a[aria-current="page"] {
  color: var(--accent-press);
  border-bottom-color: var(--accent);
}

.hos-nav-cta { display: block; }
.hos-nav-toggle { display: none; }

/* Mobile slide-in panel */
/* THE HEIGHT IS DECLARED TWICE, vh THEN dvh, AND THAT IS DELIBERATE.
   100vh on a phone is the LARGE viewport - the height the page would have if
   the browser's URL bar were retracted - so `calc(100vh - 98px)` made the menu
   taller than the screen whenever the bar was showing, which is every time the
   menu is opened from the top of a page. The last item and the socials pinned
   to `margin-top: auto` sat below the fold of a panel that is supposed to be
   exactly one screen. 100dvh is the viewport as it actually is right now and
   tracks the bar retracting.
   The vh line stays underneath as the fallback for anything that does not know
   dvh; a browser that does simply takes the second declaration. */
.hos-menu {
  position: absolute;
  inset: 98px 0 auto auto;
  height: calc(100vh - 98px);
  height: calc(100dvh - 98px);
  width: min(86vw, 360px);
  background: var(--ink-900);
  border-left: var(--border-1) solid var(--border-inverse);
  box-shadow: var(--shadow-pop);
  padding: var(--space-8) var(--space-6);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  animation: hosSlideIn var(--dur-base) var(--ease-out);
  overflow-y: auto;
}

.hos-menu[hidden] { display: none; }

.hos-menu a.hos-menu__link {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--white);
}

.hos-menu a.hos-menu__link[aria-current="page"] { color: var(--accent); }
.hos-menu a.hos-menu__link:hover { color: var(--accent); }
.hos-menu .hos-socials { margin-top: auto; }

@keyframes hosSlideIn {
  from { transform: translateX(12px); opacity: 0; }
  to { transform: none; opacity: 1; }
}

/* Six nav items clear 1240 with room to spare, but between the 900px menu
   breakpoint and ~1024 the row runs out of width. This band tightens the gaps
   and drops the badge one size rather than wrapping or overflowing; the scrolled
   state keeps its own smaller badge height. */
@media (min-width: 901px) and (max-width: 1023px) {
  .hos-header__nav,
  .hos-nav-links { gap: var(--space-4); }
  .hos-header__logo img { height: 56px; }
}

@media (max-width: 900px) {
  .hos-nav-links,
  .hos-nav-cta { display: none !important; }
  .hos-nav-toggle { display: block; }
  .hos-header__bar,
  html[data-scrolled="yes"] .hos-header__bar { height: 76px; }
  .hos-header__logo img,
  html[data-scrolled="yes"] .hos-header__logo img { height: 54px; }
  .hos-menu,
  html[data-scrolled="yes"] .hos-menu {
    top: 76px;
    height: calc(100vh - 76px);
    height: calc(100dvh - 76px);
  }
}

/* ==========================================================================
   PlatformRow: components/social/PlatformRow.jsx
   ========================================================================== */

.hos-platforms {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.hos-platforms__label { color: var(--text-muted); }
.hos-platforms--on-dark .hos-platforms__label { color: var(--gray-400); }

.hos-platforms__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.hos-platforms--chip .hos-platforms__list { gap: var(--space-2); }

.hos-platform {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  min-height: var(--tap-min);
  transition: var(--transition-interactive);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  white-space: nowrap;
}

.hos-platform--badge {
  padding: 0 18px;
  height: 52px;
  border-radius: var(--radius-md);
  border: var(--border-1) solid var(--border-hairline);
  background: var(--surface-card);
  color: var(--ink-900);
  box-shadow: var(--shadow-sm);
}

.hos-platform--badge:hover {
  border-color: var(--ink-900);
  background: var(--white);
  color: var(--accent-press);
  box-shadow: var(--shadow-card);
}

.hos-platforms--on-dark .hos-platform--badge {
  border-color: rgba(255, 255, 255, .24);
  background: transparent;
  color: var(--paper);
  box-shadow: none;
}

.hos-platforms--on-dark .hos-platform--badge:hover {
  border-color: var(--accent);
  background: rgba(255, 255, 255, .06);
  color: var(--accent);
  box-shadow: none;
}

.hos-platform--badge .hos-icon { width: 22px; height: 22px; }

.hos-platform--chip {
  width: 46px;
  height: 46px;
  min-height: 46px;
  justify-content: center;
  border-radius: var(--radius-pill);
  border: var(--border-1) solid var(--border-hairline);
  background: transparent;
  color: var(--ink-900);
}

.hos-platform--chip:hover { color: var(--accent); }

.hos-platforms--on-dark .hos-platform--chip {
  border-color: rgba(255, 255, 255, .24);
  color: var(--paper);
}

.hos-platforms--on-dark .hos-platform--chip:hover { color: var(--accent); }
.hos-platform--chip .hos-icon { width: 20px; height: 20px; }

/* PHONE: THE BADGE LOSES ITS WORD AND KEEPS ITS SHAPE.
   (Owner-directed, 2026-08-22: the hero's subscribe block "reads cluttered".)

   A badge is `white-space: nowrap`, and "APPLE PODCASTS" or "AMAZON MUSIC" at
   18px of padding is wider than half a 390px screen - so the five badges
   wrapped one per line and the hero spent about 600px of scroll on a list of
   five app names in ragged-width boxes. Icon-only they are 52px squares and
   the whole set is one row of five: 5x52 plus four 12px gaps is 308px inside
   the 342px a 390px phone offers.

   NOTHING IS LOST TO A SCREEN READER OR A CRAWLER. Every badge already carries
   aria-label="Listen on <platform>" - both hos_platform_row() in partials/
   lib.php and platformRow() in js/chrome.js emit it - so the accessible name
   is the same sentence it was with the word showing. The <span> is the visible
   duplicate of a name the element already has.

   It stays a BADGE and not the --chip variant: the rounded-square border is
   how the subscribe row is told apart from the round social row directly under
   it, and at this size that shape is the only thing left to tell them apart
   by. */
@media (max-width: 640px) {
  .hos-platform--badge {
    width: 52px;
    padding: 0;
    justify-content: center;
  }

  /* :not(.hos-icon) matters - hos_icon() renders the logo as a <span> too, so a
     bare `> span` hides the icon along with the word and leaves five empty
     boxes. Only the label span goes. */
  .hos-platform--badge > span:not(.hos-icon) { display: none; }
}

/* ==========================================================================
   SocialRow: components/social/SocialRow.jsx
   ========================================================================== */

.hos-socials {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.hos-social {
  width: 44px;
  height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-pill);
  text-decoration: none;
  border: var(--border-1) solid var(--border-hairline);
  background: transparent;
  color: var(--ink-900);
  transition: var(--transition-interactive);
}

.hos-social:hover { background: var(--surface-sunken); color: var(--accent); }

.hos-socials--on-dark .hos-social {
  border-color: rgba(255, 255, 255, .22);
  color: var(--paper);
}

.hos-socials--on-dark .hos-social:hover { background: rgba(255, 255, 255, .07); color: var(--accent); }

.hos-socials--plain .hos-social { border: none; background: transparent; }
.hos-socials--plain .hos-social:hover { background: transparent; color: var(--accent); }

.hos-social .hos-icon { width: 20px; height: 20px; }
.hos-socials--sm .hos-social .hos-icon { width: 18px; height: 18px; }

/* Config values still pending (PLACEHOLDER) render as anchors with no href so a
   click cannot jump the page. Box, colors and hover states are untouched; only
   the pointer cursor goes, because the target does not exist yet. */
.hos-platform[data-placeholder],
.hos-social[data-placeholder] { cursor: default; }

/* ==========================================================================
   NewsletterStrip: components/forms/NewsletterStrip.jsx
   ========================================================================== */

.hos-newsletter {
  background: var(--ink-900);
  border-top: var(--border-3) solid var(--accent);
  padding: var(--space-16) var(--gutter);
}

/* minmax(MIN(320px, 100%), 1fr), not minmax(320px, 1fr).
   A bare 320px floor is a MINIMUM the track cannot go under, so on a viewport
   narrower than 320px + gutters the track stays 320 and the section overflows
   its own container - which is a horizontal scrollbar on EVERY page of the
   site, because this strip is on every page. At 360px the container offers
   312px inside the 24px gutters, the track held 320, and the document measured
   368: eight pixels of sideways scroll on a small phone, both engines.
   min(320px, 100%) keeps the 320px intent wherever there is room for it and
   lets the track shrink to the container below that, which is the only width
   at which the floor was ever doing harm. Desktop layout is unchanged. */
.hos-newsletter__grid {
  display: grid;
  gap: var(--space-10);
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  align-items: center;
}

.hos-newsletter__copy {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.hos-newsletter__copy h2 { font-size: var(--text-h2); color: var(--white); }
.hos-newsletter__copy p { color: var(--gray-300); max-width: var(--measure-narrow); }

/* Phone: the strip heading steps h2 -> h3, the same step .hos-featured-player__title
   already makes on a narrow screen.

   It is here because of the copy. --text-h2 clamps at a 30px FLOOR, and at 30px
   "SUBSCRIBE TO OUR" measures 324px against the 320px a 390px viewport leaves
   inside the gutters - four pixels over, so the line breaks after "OUR" and the
   heading sets as SUBSCRIBE / TO OUR / WEEKLY EMAIL with an orphaned two-word
   middle line. The copy this replaced fitted in two lines there, so leaving the
   size alone would have made the phone strip worse to change the words.

   At 24px the whole heading is 461px: two clean lines at 390 and one line by
   the time the breakpoint lifts at 640. text-wrap: balance was tried first and
   does nothing here - it keeps the line count and picks the same break. */
@media (max-width: 640px) {
  .hos-newsletter__copy h2 { font-size: var(--text-h3); }
}

.hos-newsletter__form {
  display: flex;
  gap: var(--space-3);
  align-items: flex-end;
  flex-wrap: wrap;
}

/* The strip's live region. Shares .hos-form__status with the contact form - its
   layout, its :empty collapse, its data-state hook - and only repaints the two
   state colours, because --state-success (#1B7A3E) and --state-error (#C3200B)
   are chosen for the paper surface and are close to unreadable on ink.
   The last child of the wrapping flex form, so `flex: 1 0 100%` drops it onto
   its own line under the field and the button. It is deliberately NOT a third
   child of .hos-newsletter__grid: that grid is auto-fit/minmax(320px), which
   has room for three tracks at desktop width and collapses the empty one, so a
   third child turned the two-column strip into three with a hole in it.

   SELECTORS ARE .hos-newsletter-SCOPED, not bare .hos-newsletter__status. The
   contact form's own [data-state] rules are ~1200 lines further down this file
   at identical specificity, so a bare class here loses the tie on source order
   and the success line renders in the dark-on-dark green. */
.hos-newsletter .hos-form__status {
  flex: 1 0 100%;
  margin-top: calc(var(--space-1) * -1);
  color: var(--gray-300);
}

.hos-newsletter .hos-form__status[data-state="ok"] { color: var(--accent); }
.hos-newsletter .hos-form__status[data-state="error"] { color: var(--accent-hover); }

/* Field: components/forms/Field.jsx */
.hos-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.hos-newsletter__form .hos-field { flex: 1 1 240px; }

/* The input and the Subscribe button are ONE control row, so they are one
   height. They were not: the button is .hos-btn--md at a flat 48px, while the
   input took its height from padding plus its line box and came out at 53. The
   form is align-items: flex-end, so the bottoms agreed and the tops were 5px
   apart, which with the "EMAIL *" label stacked above the field reads as the
   button sitting high in the row - the owner's report was that the button is
   vertically offset from the input.

   48px is the button's own number, taken from .hos-btn--md rather than invented
   here, so the strip's control row is the same height as every other medium
   button on the page and the two can only ever disagree if that rule changes.
   It clears the 44px --tap-min with room to spare.

   padding-block: 0 comes with it. The shared field rule pads 12px top and
   bottom, which inside a 48px border-box leaves 22px for a line box that wants
   27 and would clip the descenders. With the block padding gone the input
   centres its own text in the 46px content area, which is what a bare input
   does anyway.

   SCOPED TO THIS FORM. The contact page's fields are a stacked column with no
   button beside them, nothing to line up with, and no reason to lose the 12px
   of breathing room the design gave them. */
.hos-newsletter__form .hos-field input {
  height: 48px;
  padding-block: 0;
}

.hos-field label { color: var(--text-body); }
.hos-field__required { color: var(--accent); }

.hos-field input,
.hos-field select,
.hos-field textarea {
  width: 100%;
  min-height: var(--tap-min);
  padding: 12px 14px;
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--text-display);
  background: var(--white);
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-md);
  outline: none;
  box-shadow: none;
  transition: var(--transition-interactive);
}

.hos-field input:focus,
.hos-field select:focus,
.hos-field textarea:focus {
  border-color: var(--accent);
  box-shadow: var(--ring-focus);
}

.hos-field--on-dark label { color: var(--gray-300); }

.hos-field--on-dark input,
.hos-field--on-dark select,
.hos-field--on-dark textarea {
  color: var(--white);
  background: rgba(255, 255, 255, .05);
  border-color: rgba(255, 255, 255, .24);
}

/* ==========================================================================
   SiteFooter: components/navigation/SiteFooter.jsx + the design file's
   footer logo override (paper chip, 180px wide)
   ========================================================================== */

.hos-footer {
  background: var(--ink-900);
  color: var(--gray-300);
  padding-top: var(--space-20);
  padding-bottom: var(--space-10);
}

.hos-footer__grid {
  display: grid;
  gap: var(--space-12);
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
}

.hos-footer__brand {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.hos-footer__logo {
  width: 180px;
  height: auto;
  background: var(--paper);
  padding: 10px 14px;
  border-radius: var(--radius-md);
  box-sizing: border-box;
}

.hos-footer__tagline {
  font-family: var(--font-display);
  font-size: var(--text-h4);
  text-transform: uppercase;
  color: var(--white);
  letter-spacing: var(--tracking-tight);
  max-width: 18ch;
}

.hos-footer__col {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.hos-footer__col .hos-eyebrow { color: var(--gray-400); }

.hos-footer__link {
  font-size: var(--text-sm);
  color: var(--gray-200);
  text-decoration: none;
}

.hos-footer__link:hover { color: var(--accent); }

/* RSS affordance: Podbean is the host, not a listening platform, so it left the
   platform row (Plan Amendment 1 I) and lives here as the raw feed link. */
.hos-footer__rss {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.hos-footer__rss .hos-icon { width: 16px; height: 16px; }

.hos-footer__email {
  font-size: var(--text-sm);
  color: var(--accent);
  text-decoration: none;
}

.hos-footer__email:hover { color: var(--accent-hover); }

.hos-footer__bottom {
  margin-top: var(--space-16);
  padding-top: var(--space-6);
  border-top: var(--border-1) solid var(--border-inverse);
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  justify-content: space-between;
  font-size: var(--text-xs);
  color: var(--gray-400);
}

/* PHONE FOOTER: link columns two-across instead of one long stack.
   (Owner-directed, 2026-08-22, from real-iPhone testing: the footer was a
   single column the visitor had to scroll past on every page.)

   auto-fit with a 240px floor can only ever make ONE track at phone width, so
   the breakpoint states the column count outright rather than lowering the
   floor - a lower floor would also re-flow the desktop footer, which is not
   what was asked for. The brand block spans both tracks and stays on top; the
   remaining cells are the link columns and the Subscribe column, which pair
   off two at a time. Row gap stays generous, column gap tightens, because the
   two tracks are ~140px each at 360px wide. */
@media (max-width: 640px) {
  .hos-footer__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-10) var(--space-6);
  }

  .hos-footer__brand { grid-column: 1 / -1; }

  /* The LAST column is the Subscribe block - the platform chip row, the RSS
     link and the mailto - and it spans both tracks rather than pairing off.
     Two reasons, both measured: the address `info@homeownersshow.com` sets at
     193px and has no break opportunity a browser will take, so in a 159px
     track it hung 10px off the right edge of the document and put a horizontal
     scrollbar on every page; and the chip row wants a line it can lay out on
     rather than one chip per row. That leaves one empty cell beside the third
     link column, which is ordinary footer whitespace at the end of a short
     list. The three LINK columns still pair two-across, which is the halving
     the owner asked for. */
  .hos-footer__grid > .hos-footer__col:last-child { grid-column: 1 / -1; }

  /* The links become 44px tap targets here (see --tap-min), so the column's
     own gap comes down to keep the pair of columns about the height the single
     column was per item rather than half again as tall. */
  .hos-footer__col { gap: var(--space-2); }
}

/* Belt and braces for the address, independent of the phone layout above: it
   is the one string in the footer with no spaces in it, so it is the one that
   decides the minimum width of whatever column it lands in. Letting it break
   means a future narrower column can never reintroduce the overflow. */
.hos-footer__email { overflow-wrap: anywhere; }

/* ==========================================================================
   HostCard: components/people/HostCard.jsx
   ========================================================================== */

.hos-host-card {
  display: flex;
  flex-direction: column;
  gap: 0;
  background: var(--surface-card);
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: var(--transition-interactive);
}

.hos-host-card:hover { box-shadow: var(--shadow-card-hover); }

.hos-host-card--on-dark {
  background: var(--surface-inverse-card);
  border-color: var(--border-inverse);
  box-shadow: none;
}

.hos-host-card--on-dark:hover { box-shadow: none; }

.hos-host-card__media {
  position: relative;
  aspect-ratio: 4 / 5;
  background: var(--ink-800);
  overflow: hidden;
}

.hos-host-card__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 22%;
  filter: saturate(.92) contrast(1.04);
  transform: scale(1);
  transition: transform var(--dur-slow) var(--ease-out);
}

.hos-host-card:hover .hos-host-card__media img { transform: scale(1.03); }

.hos-host-card__body {
  padding: var(--card-pad-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  justify-content: center;
}

.hos-host-card__name {
  font-size: var(--text-h2);
  color: var(--text-display);
}

.hos-host-card--on-dark .hos-host-card__name { color: var(--white); }

.hos-host-card__bio {
  font-size: var(--text-sm);
  color: var(--text-body);
  max-width: var(--measure-body);
}

.hos-host-card--on-dark .hos-host-card__bio { color: var(--gray-300); }

.hos-host-card__fact {
  display: flex;
  gap: var(--space-2);
  align-items: flex-start;
  margin-top: var(--space-2);
  padding-top: var(--space-4);
  border-top: var(--border-1) solid var(--divider);
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.hos-host-card--on-dark .hos-host-card__fact {
  border-top-color: var(--border-inverse);
  color: var(--gray-400);
}

.hos-host-card__fact .hos-icon {
  width: 17px;
  height: 17px;
  color: var(--accent);
  margin-top: 3px;
}

/* layout="row": desktop only; stacks under 900px like the design's phone view */
@media (min-width: 901px) {
  .hos-host-card--row { flex-direction: row; gap: var(--space-8); }
  .hos-host-card--row .hos-host-card__media { flex: 0 0 38%; aspect-ratio: auto; }
}

/* ==========================================================================
   FeaturedPlayer: components/episodes/FeaturedPlayer.jsx

   The design system component wrapped its own <audio>; this port drives the
   ONE shared element owned by js/player.js instead, so the hero player and the
   sticky mini player are always the same playback. Everything below is layout
   only - js/pages/home.js fills it and keeps the transport in sync.
   ========================================================================== */

.hos-featured-player {
  background: var(--surface-inverse-card);
  border: var(--border-1) solid var(--border-inverse);
  border-radius: var(--radius-lg);
  padding: var(--card-pad-lg);
  display: grid;
  gap: var(--space-8);
  grid-template-columns: minmax(180px, 260px) 1fr;
}

/* 16:9, mapped or not, for the same reason .hos-episode-card__art is - see the
   long note there. The hero is the one box where the square was arguable, since
   it has no sibling to line up with, but the badge sitting square up here and
   16:9 in every card below it read as two different components rather than one
   show. Consistency won. See that rule for why the padding is asymmetric. */
.hos-featured-player__art {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--white);
}

.hos-featured-player__art img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 6% 10%;
}

.hos-featured-player__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  min-width: 0;
}

.hos-featured-player__eyebrows {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
}

.hos-featured-player__dot {
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  background: var(--accent);
}

.hos-featured-player__eyebrow { color: var(--accent); }
.hos-featured-player__date { color: var(--gray-400); }

.hos-featured-player__title {
  font-size: var(--text-h2);
  color: var(--white);
  letter-spacing: var(--tracking-display);
}

.hos-featured-player__summary {
  font-size: var(--text-base);
  color: var(--gray-300);
  max-width: var(--measure-body);
}

.hos-featured-player__controls {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-2);
}

.hos-featured-player__progress {
  flex: 1;
  min-width: 0;
}

.hos-featured-player__track {
  height: 6px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, .16);
  position: relative;
  cursor: pointer;
}

/* Seeking needs a loaded episode; until then the track is a read-only meter. */
.hos-featured-player__track[aria-disabled="true"] { cursor: default; }

.hos-featured-player__fill {
  position: absolute;
  inset: 0;
  right: 100%;
  background: var(--accent);
  border-radius: var(--radius-pill);
}

.hos-featured-player__times {
  display: flex;
  justify-content: space-between;
  margin-top: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--gray-400);
  font-variant-numeric: tabular-nums;
}

.hos-featured-player__skips {
  display: flex;
  gap: var(--space-2);
}

.hos-featured-player__skips .hos-icon-btn { border-radius: var(--radius-pill); }
.hos-featured-player__skips .hos-icon-btn[disabled] { opacity: .45; cursor: default; }
.hos-featured-player__skips .hos-icon-btn[disabled]:hover {
  background: transparent;
  color: var(--paper);
}

.hos-featured-player__foot {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding-top: var(--space-4);
  border-top: var(--border-1) solid var(--border-inverse);
}

.hos-featured-player__notes {
  margin-left: auto;
  color: var(--accent);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.hos-featured-player__notes:hover { color: var(--accent-hover); }
.hos-featured-player__notes .hos-icon { width: 15px; height: 15px; }

/* compact={isPhone}: single column, smaller art, 56px transport, no skips. */
@media (max-width: 900px) {
  .hos-featured-player { grid-template-columns: 1fr; padding: var(--card-pad); }
  .hos-featured-player__art { max-width: 200px; }
  .hos-featured-player__title { font-size: var(--text-h3); }
  .hos-featured-player .hos-play-btn--lg { --play-size: 56px; }
  .hos-featured-player__skips { display: none; }
  .hos-featured-player__notes { margin-left: 0; }

  /* THE LABEL LEADS THE CARD ON A PHONE.
     (Owner-directed, 2026-08-22, from his own iPhone.)

     Side by side the eyebrow row is the top of the RIGHT column, so it reads
     first anyway. Stacked, the art goes above the body and 'LATEST EPISODE ·
     EP 357' ends up underneath the artwork - so on the home page the card
     arrived after the Follow-the-show block with no label on it at all and
     read as a picture floating free of its section.

     `display: contents` on the body is what makes this a two-line fix instead
     of a restructure: it dissolves the wrapper so its children become items of
     the article's own grid, and the eyebrow row - now a sibling of the art -
     can take order:-1 and sit above it. Everything else keeps source order.
     The body is a plain <div> with no semantics to lose, and the article's gap
     steps down to the space-4 the body itself was using between its children,
     because that gap now has to serve both jobs. */
  .hos-featured-player { gap: var(--space-4); }
  .hos-featured-player__body { display: contents; }
  .hos-featured-player__eyebrows { order: -1; }

  /* The promoted children are grid items now, and a grid item's default
     min-width is auto - which lets a long word push the card wider than its
     column. The body used to carry this for them. */
  .hos-featured-player__title,
  .hos-featured-player__summary,
  .hos-featured-player__controls,
  .hos-featured-player__foot { min-width: 0; }
}

/* ==========================================================================
   EpisodeCard: components/episodes/EpisodeCard.jsx (layout="grid")

   Shared by the home 3-up grid and, later, the episodes index. The summary is
   clamped to 3 lines: live feed summaries run to 800+ characters and would
   otherwise set the height of the whole row.
   ========================================================================== */

.hos-episode-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  background: var(--surface-card);
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--card-pad);
  box-shadow: var(--shadow-card);
  transition: var(--transition-interactive);
}

.hos-episode-card:hover {
  border-color: var(--gray-300);
  box-shadow: var(--shadow-card-hover);
  transform: translateY(var(--lift-y));
}

/* EVERY card art box is 16:9, mapped or not.
   ------------------------------------------
   This box used to be 1:1 and only the thumbnail variant below reshaped it to
   16:9, which meant a grid holding both a mapped and an unmapped episode had
   two card heights in the same row - the owner's words were that it "looks a
   little out of place". Uniformity of the grid beats the art box being drawn
   for the badge's native square: the badge is a mark with transparent margin,
   so `contain` re-fits it into whatever frame it is given, whereas a card
   whose art is 140px taller than the card beside it has no way to recover.

   So the SHAPE lives here, on the base class, and the thumbnail class below is
   reduced to the fit switch alone. Same-shape-always also makes the img's
   onerror path a no-op for layout: dropping .hos-card-art--thumb swaps the
   photograph's `cover` back to the badge's `contain` without the card resizing
   underneath the reader. */
.hos-episode-card__art {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
  flex: 0 0 auto;
  overflow: hidden;
  border-radius: var(--radius-md);
  background: var(--white);
}

/* Asymmetric padding, and it has to be: a percentage padding resolves against
   the INLINE size on all four sides, so the old symmetric 10% took 10% of the
   width off the top and the bottom too. In a square box that is 10% of the
   height; in a 16:9 box it is 17.8% of it, per side, which shrank the badge to
   roughly half the box. 6% of the width is ~10.7% of a 16:9 height, so the
   badge lands at about 79% of the box height - the same optical weight it had
   in the square - while the sides keep the 10% they were drawn with. */
.hos-episode-card__art img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 6% 10%;
}

/* YouTube thumbnail in the shared 16:9 art box.
   ---------------------------------------------
   The badge is a mark on white and wants object-fit: contain plus the padding
   above; a thumbnail is a photograph and wants to fill the frame. This class is
   that switch, and nothing else - partials/episode_lib.php and js/cards.js add
   it only when a thumbnail is actually being emitted, and the img's onerror
   removes it again if the thumbnail fails to load.

   THE BOX IS 16:9 FOR BOTH, and that shape is set on the base classes above
   rather than here. hqdefault.jpg is 480x360 - a 16:9 image letterboxed into
   4:3 with a black bar top and bottom. Measured on three real thumbnails: the
   bars are 45px each and the content is exactly 480x270, which is 16:9 to the
   pixel.

   `cover` fills the box on its tight axis and crops the other. In a 1:1 box the
   tight axis is height, so the whole 360px - bars included - is kept and 25% of
   the WIDTH is thrown away, 12.5% off each side. These are title cards: the
   channel sets its headline hard against the left edge ("Full Review of the NEW
   Gozney Arc Lite", "Debunking Safe Myths"), so a 12.5% left crop takes the
   first word or two off every one of them. The bars stayed, the words went.

   At 16:9 the tight axis becomes width. The image scales to 480 wide, fills the
   box edge to edge, and `cover` crops the vertical overflow - which is the two
   45px bars, and nothing else. Zero horizontal loss. The letterbox goes and the
   headline stays.

   Because the shape no longer moves with the class, the onerror path costs the
   reader nothing: the box stays put and only the fit changes underneath it.

   Placed after .hos-episode-card__art / .hos-featured-player__art and their img
   rules: same specificity (0,1,0 and 0,1,1), so source order is what decides
   it. */
.hos-card-art--thumb img {
  object-fit: cover;
  padding: 0;
}

.hos-episode-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex: 1;
  min-width: 0;
}

.hos-episode-card__meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-3);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

.hos-episode-card__number { color: var(--accent-press); }

.hos-episode-card__duration {
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.hos-episode-card__duration .hos-icon { width: 13px; height: 13px; }

.hos-episode-card__title {
  font-size: var(--text-h4);
  color: var(--text-display);
  letter-spacing: var(--tracking-tight);
}

.hos-episode-card__title a { color: inherit; text-decoration: none; }
.hos-episode-card__title a:hover { color: var(--accent-press); }

.hos-episode-card__summary {
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  color: var(--text-muted);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
}

.hos-episode-card__actions {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: auto;
  padding-top: var(--space-2);
}

.hos-episode-card__notes {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ink-900);
  display: inline-flex;
  align-items: center;
  gap: 6px;
}

.hos-episode-card__notes:hover { color: var(--accent-press); }
.hos-episode-card__notes .hos-icon { width: 15px; height: 15px; }

/* The chip PlatformRow the design hangs off the right end of a row card
   (showPlatforms). The design decided this from a JS phone check; here the
   breakpoint below does it, so a resize behaves like a reload. */
.hos-episode-card__platforms {
  display: none;
  margin-left: auto;
}

/* layout="row": the episodes index list. Desktop only - under 900px the design
   drops to the grid card, which is exactly this card without the modifier. */
@media (min-width: 901px) {
  .hos-episode-card--row {
    flex-direction: row;
    align-items: flex-start;
    gap: var(--space-6);
  }

  .hos-episode-card--row .hos-episode-card__art { width: 140px; flex: 0 0 140px; }
  .hos-episode-card--row .hos-episode-card__title { font-size: var(--text-h3); }
  .hos-episode-card--row .hos-episode-card__platforms { display: block; }
}

/* ==========================================================================
   Tag: components/core/Tag.jsx, neutral tone - the episodes topic chips
   ========================================================================== */

.hos-tag {
  display: inline-flex;
  align-items: center;
  height: 32px;
  padding: 0 12px;
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-body);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  cursor: pointer;
  transition: var(--transition-interactive);
}

.hos-tag:hover { background: var(--gray-100); }

.hos-tag--active {
  background: var(--ink-900);
  border-color: var(--ink-900);
  color: var(--paper);
}

.hos-tag--active:hover { background: var(--ink-900); }

/* A topic tag that is a real anchor to its /topics/<slug> hub, rather than a
   label. Only the base .hos-tag look plus the affordances an anchor needs: the
   element is an <a>, so the browser gives it the underline and the visited
   colour this removes, and .hos-tag--on-dark sets `cursor: default` for the
   label case, which a link has to take back. */
.hos-tag--link {
  cursor: pointer;
  text-decoration: none;
  color: inherit;
}

.hos-tag--link:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.hos-tag--on-dark.hos-tag--link:hover {
  background: transparent;
  border-color: var(--accent);
  color: var(--accent);
}

/* THE TOPIC CHIP CAPSULE on /episodes: the filter <button> and, for a topic
   that has a published hub, an icon-only <a> to it - inside ONE pill.

   These were two pills 4px apart until the owner looked at the row on a phone:
   fourteen chips plus twelve detached arrows reads as twenty-six objects, and
   an unlabelled 32px arrow floating beside a chip reads as a mystery button.
   They are one control's two halves, so they now look like it. See the block
   comment in episodes.php for why they are still two ELEMENTS.

   The capsule owns the border and the pill radius; the zones inside it are
   transparent and borderless, so the height is 32px exactly as before - the
   zones are 100% of the capsule's 30px content box. The divider is the only new
   line on the page, and it is the same hairline token as the capsule's own
   border. */
.hos-chip {
  display: inline-flex;
  align-items: center;
  height: 32px;
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-pill);
  transition: var(--transition-interactive);
}

.hos-chip .hos-tag {
  height: 100%;
  border: 0;
  background: transparent;
  border-radius: var(--radius-pill) 0 0 var(--radius-pill);
}

.hos-chip__hub {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  min-width: 40px;
  border: 0;
  border-inline-start: var(--border-1) solid var(--divider);
  border-radius: 0 var(--radius-pill) var(--radius-pill) 0;
  background: transparent;
  color: var(--text-muted);
  text-decoration: none;
  cursor: pointer;
  transition: var(--transition-interactive);
}

.hos-chip__hub .hos-icon { width: 14px; height: 14px; }

.hos-chip__hub:hover {
  background: var(--gray-100);
  color: var(--accent);
}

/* ACTIVE IS THE WHOLE CAPSULE, said twice on purpose. :has() paints the capsule
   itself, border included, which is the version a modern browser renders. The
   two zone rules under it paint the same fill on the two children, which is
   what a browser without :has() falls back to - identical inside, with the
   hairline ring left over instead of an ink one. */
.hos-chip:has(.hos-tag--active) {
  background: var(--ink-900);
  border-color: var(--ink-900);
}

.hos-chip .hos-tag--active {
  background: var(--ink-900);
  color: var(--paper);
}

.hos-tag--active + .hos-chip__hub {
  background: var(--ink-900);
  color: var(--paper);
  border-inline-start-color: var(--border-inverse);
}

.hos-tag--active + .hos-chip__hub:hover {
  background: var(--ink-900);
  color: var(--accent);
}

/* ==========================================================================
   Episodes index: chip row, card list, empty state, load more
   ========================================================================== */

.hos-chip-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-8);
}

.hos-episodes-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

.hos-episodes-empty {
  margin-top: var(--space-6);
  font-size: var(--text-base);
  color: var(--text-muted);
}

.hos-episodes-more {
  display: flex;
  justify-content: center;
  margin-top: var(--space-10);
}

.hos-episodes-more[hidden] { display: none; }

/* The archive is server-rendered whole (Plan Amendment 2): every episode is an
   anchor in the document, which is the internal link graph an AI crawler
   follows. What the visitor sees is a WINDOW onto that list - four cards, then
   four more per "Load more" - and this class is how the window is expressed.
   A class rather than absence, deliberately: absence is exactly what made 350-odd
   episodes invisible to a crawler before, and re-introducing it for the ones
   below the fold would give back most of what the amendment bought.
   js/pages/episodes.js is the only thing that adds or removes it. */
.hos-episode-card--offscreen { display: none; }

/* ==========================================================================
   MerchCard: components/commerce/MerchCard.jsx

   Amendment 1 ruling 3: no hardcoded price anywhere on the site - the tee's
   price is set on Amazon, so the price slot carries a "Price on Amazon" note.
   ========================================================================== */

.hos-merch-card {
  display: flex;
  flex-direction: column;
  background: var(--surface-card);
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: var(--shadow-card);
  transition: var(--transition-interactive);
}

.hos-merch-card:hover {
  border-color: var(--gray-300);
  box-shadow: var(--shadow-card-hover);
  transform: translateY(var(--lift-y));
}

.hos-merch-card__media {
  aspect-ratio: 1 / 1;
  background: var(--surface-sunken);
  display: grid;
  place-items: center;
  overflow: hidden;
}

.hos-merch-card__media img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  padding: 14%;
}

.hos-merch-card__body {
  padding: var(--card-pad);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex: 1;
}

.hos-merch-card__name { font-size: var(--text-h4); }

.hos-merch-card__note {
  font-size: var(--text-sm);
  color: var(--text-muted);
}

.hos-merch-card__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  margin-top: auto;
  padding-top: var(--space-3);
}

/* ==========================================================================
   FAQ list + closing CTA list (Plan Amendment 1, section C)

   Neither section exists in the design file; both are built from design tokens
   so they read as part of the same system.
   ========================================================================== */

.hos-faq { display: flex; flex-direction: column; }

.hos-faq__item {
  padding-block: var(--space-6);
  border-top: var(--border-1) solid var(--divider);
}

.hos-faq__item:first-child { border-top: 0; padding-top: 0; }
.hos-faq__item:last-child { padding-bottom: 0; }

/* Each item is a native <details>: the disclosure needs no script, and the
   answer stays in the served HTML whether the item is open or closed, so the
   FAQPage node still describes text a crawler can read.

   The summary's padding is cancelled by an equal negative margin. That buys a
   44px+ (--tap-min) hit target for the question without moving a single pixel
   of the divided-stack rhythm above it or the answer below it. */
.hos-faq__summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  padding-block: var(--space-3);
  margin-block: calc(var(--space-3) * -1);
  cursor: pointer;
  list-style: none;               /* Firefox / spec marker */
}

.hos-faq__summary::-webkit-details-marker { display: none; }  /* Safari marker */

/* The chevron is the only affordance, so it carries the state: down when the
   item is closed, flipped up when it is open. Duration comes from the motion
   tokens, which collapse to 0ms under prefers-reduced-motion, so the reduced
   -motion case is handled by the same rule rather than a second one. */
.hos-faq__summary .hos-icon {
  width: 20px;
  height: 20px;
  color: var(--text-muted);
  transition: transform var(--dur-fast) var(--ease-standard);
}

.hos-faq__item[open] .hos-faq__summary .hos-icon { transform: rotate(180deg); }

.hos-faq__summary:hover .hos-icon { color: var(--accent); }

.hos-faq__q {
  font-size: var(--text-h4);
  color: var(--text-display);
}

.hos-faq__a {
  margin-top: var(--space-3);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--text-body);
  max-width: var(--measure-long);
}

/* Links inside prose keep their underline; the site-wide rule strips it for
   buttons and nav, which would leave these invisible in a paragraph. */
.hos-faq__a a {
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

/* Same job on an inverse section: the site-wide link color is --accent-press,
   which is too dark on ink, and its hover is --ink-900, which is invisible. */
.hos-link-on-dark {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

.hos-link-on-dark:hover { color: var(--accent-hover); }

.hos-cta-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
}

.hos-cta-list__item {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  flex-wrap: wrap;
  padding-top: var(--space-6);
  border-top: var(--border-1) solid var(--divider);
}

.hos-cta-list__item:first-child { border-top: 0; padding-top: 0; }

/* THE PROMPT IS A LABEL FOR THE BUTTON BESIDE IT, so it is set like one.
   (Owner-directed, 2026-08-22: the three prompts in the closing CTA "get lost
   as plain body text".)

   They did: at --text-lead in --text-body they were the same weight and colour
   as every paragraph above them, and on a phone - where the button wraps below
   its prompt rather than sitting beside it - each one read as a stray sentence
   rather than as the question its button answers.

   Bold at display colour rather than the eyebrow treatment. The eyebrow is
   uppercase, 12px and letterspaced, which is right for a category name and
   wrong for 'Got a question about your house?' - a full sentence set that way
   reads as shouting and loses its question mark in the tracking. Weight and
   colour promote the line without changing what it is. Both are tokens. */
.hos-cta-list__text {
  font-size: var(--text-lead);
  font-weight: var(--weight-bold);
  color: var(--text-display);
  flex: 1 1 22ch;
}

/* ==========================================================================
   Definition list (Plan Amendment 1, section G: the Sponsorship placements)

   Same divided-stack rhythm as .hos-faq above, but the term and its
   description share one line so the pairing reads as a single sentence.
   ========================================================================== */

.hos-deflist { display: flex; flex-direction: column; margin: 0; }

.hos-deflist__item {
  padding-block: var(--space-5);
  border-top: var(--border-1) solid var(--divider);
  max-width: var(--measure-long);
}

.hos-deflist__item:first-child { border-top: 0; padding-top: 0; }
.hos-deflist__item:last-child { padding-bottom: 0; }

/* Inline, not block: the description continues the term's line and wraps under
   it, which keeps the em dash reading as punctuation instead of a bullet. */
.hos-deflist__term {
  display: inline;
  font-family: var(--font-display);
  font-size: var(--text-h4);
  text-transform: uppercase;
  letter-spacing: var(--tracking-display);
  color: var(--text-display);
}

.hos-deflist__desc {
  display: inline;
  margin: 0;
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--text-body);
}

/* ==========================================================================
   Episode detail (Task 7): the design file's isEpisode block

   Dark hero (back button, topic Tags, FeaturedPlayer) -> light detail grid
   (show notes + optional transcript, sticky aside) -> dark "More episodes".
   The FeaturedPlayer and EpisodeCard rules above are reused as-is; only the
   pieces this page introduces live here.
   ========================================================================== */

.hos-episode-hero__top { display: flex; }

.hos-episode-hero__tags {
  display: flex;
  gap: var(--space-2);
  flex-wrap: wrap;
}

/* Tag tone="onDark": the topic chips on the ink hero. Non-interactive here -
   these label the episode, they do not filter anything. */
.hos-tag--on-dark {
  background: transparent;
  border-color: rgba(255, 255, 255, .24);
  color: var(--gray-200);
  cursor: default;
}

.hos-tag--on-dark:hover { background: transparent; }

/* EpisodeCard onDark={true}: the "More episodes" rail. The design drops the
   card's shadow and lift on ink and keeps only the border change on hover. */
.hos-episode-card--on-dark {
  background: var(--surface-inverse-card);
  border-color: var(--border-inverse);
  box-shadow: none;
}

.hos-episode-card--on-dark:hover {
  border-color: rgba(255, 255, 255, .28);
  box-shadow: none;
  transform: none;
}

.hos-episode-card--on-dark .hos-episode-card__meta { color: var(--gray-400); }
.hos-episode-card--on-dark .hos-episode-card__number { color: var(--accent); }
.hos-episode-card--on-dark .hos-episode-card__title { color: var(--white); }
.hos-episode-card--on-dark .hos-episode-card__title a:hover { color: var(--accent); }
.hos-episode-card--on-dark .hos-episode-card__summary { color: var(--gray-300); }
.hos-episode-card--on-dark .hos-episode-card__notes { color: var(--gray-200); }
.hos-episode-card--on-dark .hos-episode-card__notes:hover { color: var(--accent); }

/* -------------------------------------------------------- the detail grid */

.hos-detail-grid {
  display: grid;
  gap: var(--space-20);
  grid-template-columns: minmax(0, 1fr) 280px;
  align-items: start;
}

.hos-detail-main {
  display: flex;
  flex-direction: column;
  gap: var(--space-12);
}

.hos-detail-aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  position: sticky;
  top: 100px;
}

.hos-detail-aside__runtime {
  border-top: var(--border-1) solid var(--divider);
  padding-top: var(--space-6);
}

.hos-detail-aside__runtime p {
  font-family: var(--font-display);
  font-size: var(--text-h3);
  color: var(--text-display);
  margin-top: 8px;
}

.hos-notes {
  margin-top: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-6);
  max-width: var(--measure-long);
}

.hos-notes p {
  font-size: var(--text-lead);
  line-height: var(--leading-long);
  color: var(--text-body);
}

/* ==========================================================================
   TranscriptBlock: components/episodes/TranscriptBlock.jsx

   Rendered only when a transcript source actually resolved - js/pages/episode.js
   removes the whole <section> otherwise, so none of this ever paints an empty
   heading. Collapsed to 320px with a fade until "Read transcript".
   ========================================================================== */

.hos-transcript__head {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  margin-bottom: var(--space-6);
}

.hos-transcript__title {
  font-size: var(--text-h3);
  color: var(--text-display);
}

.hos-transcript__body {
  position: relative;
  overflow: hidden;
}

.hos-transcript__body[data-collapsed="true"] { max-height: 320px; }
.hos-transcript__body[data-collapsed="false"] { max-height: none; }

/* PARAGRAPH BLOCKS, not caption rows. A VTT cue is a subtitle unit - a couple
   of seconds sized to fit a video frame - so one row per cue rendered this
   section as a column of sentence fragments. transcript.php and episode.js now
   group consecutive cues from one speaker into a paragraph (see
   HOS_TRANSCRIPT_GAP_SECONDS), and the cue survives as an inline span inside
   it, which is what keeps click-to-seek at sentence precision. */
.hos-transcript__blocks {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
  max-width: var(--measure-long);
}

.hos-transcript__block {
  display: grid;
  grid-template-columns: minmax(84px, 110px) 1fr;
  gap: var(--space-5);
}

/* The cue inside the paragraph. The affordance has to read as "clickable" while
   a page carries hundreds of them, so it is nothing at rest: no border, no
   background, no underline. The hover state is the whole signal. */
.hos-transcript__cue {
  cursor: pointer;
  border-radius: var(--radius-sm);
  text-underline-offset: 3px;
  text-decoration-thickness: var(--border-1);
  transition: var(--transition-interactive);
}

.hos-transcript__cue:hover {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  color: var(--text-display);
}

/* The cue the playhead is inside. An inline mark on the words being spoken now
   that a cue is not a row: a left-margin rule cannot point at the middle of a
   paragraph. Tinted rather than filled, and it keeps the body text colour, so a
   long block stays as readable highlighted as not. */
.hos-transcript__cue[data-active="true"] {
  background: var(--accent-tint);
  box-shadow: 0 0 0 2px var(--accent-tint);
}

/* No pointer promise where a click cannot do anything - syncPlayability() marks
   every seek target when the feed never gave the client this episode. */
.hos-transcript__cue[aria-disabled="true"] { cursor: default; }
.hos-transcript__cue[aria-disabled="true"]:hover { text-decoration: none; color: inherit; }

.hos-transcript__aside {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.hos-transcript__speaker {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* The design's SPEAKER_COLORS map, keyed on the uppercased name. */
.hos-transcript__speaker[data-speaker="CRAIG"] { color: var(--accent-press); }
.hos-transcript__speaker[data-speaker="KEVIN"] { color: var(--ink-900); }

.hos-transcript__time {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 4px;
  color: var(--text-faint);
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-variant-numeric: tabular-nums;
  transition: var(--transition-interactive);
}

.hos-transcript__time:hover { color: var(--accent-press); }
.hos-transcript__time .hos-icon { width: 11px; height: 11px; }

.hos-transcript__text {
  font-size: var(--text-base);
  line-height: var(--leading-long);
  color: var(--text-body);
}

.hos-transcript__fade {
  position: absolute;
  inset: auto 0 0 0;
  height: 140px;
  pointer-events: none;
  background: linear-gradient(to top, var(--surface-page) 12%, rgba(247, 245, 242, 0) 100%);
}

.hos-transcript__body[data-collapsed="false"] .hos-transcript__fade { display: none; }

.hos-transcript__foot { margin-top: var(--space-6); }

/* ---- The transcript escape bar ------------------------------------------
   A floating pill that exists only while the transcript is expanded, and only
   on a phone. Owner-directed, 2026-08-22: "once expanded it's an endless
   scroll with no way out."

   It was. Some episodes run over an hour, and the only exit was the Collapse
   button at the very BOTTOM of the expanded body - so getting out meant
   flick-scrolling to the end of the thing you wanted to leave.

   SHOWN BY A SIBLING SELECTOR, NOT BY SCRIPT. `data-collapsed` on the body is
   already the one true record of expanded-or-not, so the bar reads it directly
   rather than carrying a second flag that would have to be kept in step with
   it. display:none while collapsed also keeps both buttons out of the tab
   order without needing an `inert` of their own.

   PHONE ONLY (the 900px breakpoint below). On a desktop the scrollbar shows
   you where you are, Home and End work, and a floating pill over the prose
   would be clutter solving a problem that is not there.

   IT CLEARS THE MINI PLAYER via :has(). The player bar is a sticky sibling at
   the bottom of <body> and is 68px tall on a phone; without this the escape
   pill would sit on top of the transport. :has() is the only way to ask "is
   the player showing" from here, since the bar is neither an ancestor nor a
   preceding sibling of this element. A browser that does not support :has()
   ignores the whole rule and gets the base offset, which overlaps the player
   only while something is playing - a graceful loss, not a broken layout. */
.hos-transcript__escape { display: none; }

.hos-transcript__body[data-collapsed="false"] ~ .hos-transcript__escape {
  display: none;   /* the phone breakpoint below turns it on */
}

@media (max-width: 900px) {
  .hos-transcript__body[data-collapsed="false"] ~ .hos-transcript__escape {
    display: flex;
    position: fixed;
    z-index: 65;   /* under the mini player (70), over the page */
    right: var(--gutter);
    bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
    gap: var(--space-1);
    padding: var(--space-1);
    border-radius: var(--radius-pill);
    background: rgba(14, 14, 15, .92);
    backdrop-filter: var(--blur-glass);
    -webkit-backdrop-filter: var(--blur-glass);
    border: var(--border-1) solid var(--border-inverse);
    box-shadow: var(--shadow-pop);
  }

  body:has(#mini-player:not([hidden]))
    .hos-transcript__body[data-collapsed="false"] ~ .hos-transcript__escape {
    bottom: calc(68px + var(--space-3) + env(safe-area-inset-bottom, 0px));
  }
}

.hos-transcript__escape-btn {
  min-width: var(--tap-min);
  min-height: var(--tap-min);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 var(--space-3);
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--paper);
  cursor: pointer;
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  transition: var(--transition-interactive);
}

.hos-transcript__escape-btn .hos-icon { width: 18px; height: 18px; }
.hos-transcript__escape-btn:hover { color: var(--accent); }

/* The accent fill marks the one that ENDS the transcript, so the two controls
   are told apart by more than their labels at a glance. */
.hos-transcript__escape-btn--wide {
  background: var(--accent);
  color: var(--text-on-accent);
}

.hos-transcript__escape-btn--wide:hover {
  background: var(--accent-hover);
  color: var(--text-on-accent);
}

/* Phone: the timestamp column stops being a column and rides above the text. */
@media (max-width: 640px) {
  .hos-transcript__block { grid-template-columns: 1fr; gap: var(--space-2); }
  .hos-transcript__aside { flex-direction: row; align-items: center; gap: var(--space-3); }
}

/* ==========================================================================
   ShareRow: components/social/ShareRow.jsx
   ========================================================================== */

.hos-share {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.hos-share__label { color: var(--text-muted); margin-right: var(--space-2); }

.hos-share__btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  flex: 0 0 44px;
  padding: 0;
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-body);
  cursor: pointer;
  transition: var(--transition-interactive);
}

.hos-share__btn:hover {
  background: var(--ink-900);
  border-color: var(--ink-900);
  color: var(--paper);
}

.hos-share__btn[data-copied="true"] {
  background: var(--accent-tint);
  border-color: var(--accent-tint);
  color: var(--accent-press);
}

.hos-share__btn .hos-icon { width: 20px; height: 20px; }

/* ==========================================================================
   ContactForm + its page layout (Task 8)

   Ports components/forms/ContactForm.jsx and components/forms/Field.jsx, whose
   field chrome (.hos-field) is already defined above for the newsletter strip.
   The React component held `sent` in state and swapped in a success line; here
   the same two states are one live region the form's JS writes into, so a
   failed POST has somewhere to say so without navigating.
   ========================================================================== */

.hos-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  background: var(--surface-card);
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-md);
  padding: var(--card-pad-lg);
  box-shadow: var(--shadow-card);
}

.hos-form__heading {
  font-size: var(--text-h3);
  color: var(--text-display);
}

.hos-form__note {
  font-size: var(--text-sm);
  color: var(--text-muted);
  max-width: var(--measure-narrow);
}

/* auto-fit rather than a breakpoint: Name and Email sit side by side until the
   card itself is too narrow, which is what the design's grid does. */
/* min(220px, 100%) for the same reason .hos-newsletter__grid carries the
   guard: inside the card's padding a 320px phone leaves ~224px, so a bare
   220px floor is four pixels from overflowing the form off the page. */
.hos-form__row {
  display: grid;
  gap: var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
}

.hos-form textarea {
  resize: vertical;
  line-height: var(--leading-body);
}

/* The select keeps the design's custom chevron, so it needs the native arrow
   gone and room on the right for the drawn one. */
.hos-field__select {
  position: relative;
  display: block;
}

.hos-field__select select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 40px;
}

.hos-field__chevron {
  position: absolute;
  right: 14px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  color: var(--text-muted);
  pointer-events: none;
}

.hos-form__actions {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
}

/* One live region for both outcomes. It is empty (and takes no space) until the
   POST resolves, so the card does not jump on first paint. */
.hos-form__status {
  font-size: var(--text-sm);
  font-weight: 600;
}

.hos-form__status:empty { display: none; }
.hos-form__status[data-state="ok"] { color: var(--state-success); }
.hos-form__status[data-state="error"] { color: var(--state-error); }

.hos-form button[disabled] {
  opacity: .6;
  cursor: progress;
}

/* Contact page: the design's two-column split (form + aside). Wider than the
   episode detail grid's fixed 280px rail because the aside carries prose. */
.hos-contact-grid {
  display: grid;
  gap: var(--space-16);
  grid-template-columns: minmax(0, 1.4fr) minmax(260px, .6fr);
  align-items: start;
}

.hos-contact-aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-10);
}

.hos-contact-aside p {
  margin-top: var(--space-3);
  color: var(--text-body);
}

.hos-contact-aside__email {
  font-size: var(--text-lead);
}

/* Merch page: the design's card-plus-column split. The card column is capped so
   a single product card never stretches to half the page. */
.hos-merch-grid {
  display: grid;
  gap: var(--space-16);
  grid-template-columns: minmax(0, 320px) minmax(0, 1fr);
  align-items: start;
}

/* ==========================================================================
   LiveSection: rendered into #live-slot by js/pages/home.js (Task 9)

   Never in the static markup and never in the DOM unless api/live.php reports a
   stream in progress - "not live" is an empty slot, not a hidden element, so
   nothing here needs a display:none state.

   The panel deliberately borrows the FeaturedPlayer's surface, border, radius
   and padding. It sits directly above the hero, and a second dark card with its
   own geometry would read as a different site.
   ========================================================================== */

.hos-live {
  background: var(--ink-900);
  padding-block: var(--section-y);
}

.hos-live__panel {
  background: var(--surface-inverse-card);
  border: var(--border-1) solid var(--border-inverse);
  border-radius: var(--radius-lg);
  padding: var(--card-pad-lg);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Same pill the hero opens with, so the two stack as one voice. */
.hos-live__badge {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 8px 16px;
  border-radius: var(--radius-pill);
  border: var(--border-1) solid rgba(255, 255, 255, .22);
  background: rgba(255, 255, 255, .05);
  color: var(--accent);
}

.hos-live__dot {
  width: 8px;
  height: 8px;
  flex: 0 0 auto;
  border-radius: var(--radius-pill);
  background: var(--accent);
  animation: hos-live-pulse 1.6s var(--ease-standard) infinite;
}

@keyframes hos-live-pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50%      { opacity: .35; transform: scale(.7); }
}

/* The only looping animation on the site, so it gets the only explicit opt-out.
   The dot is decoration - the words next to it carry the meaning - so reduced
   motion simply stops it rather than substituting another effect. */
@media (prefers-reduced-motion: reduce) {
  .hos-live__dot { animation: none; }
}

.hos-live__title {
  font-size: var(--text-h2);
  color: var(--white);
  letter-spacing: var(--tracking-display);
}

/* aspect-ratio on the wrapper, not the iframe: YouTube's embed ignores a height
   it is not given in pixels, so the box has to own the 16:9 and the frame just
   fills it. */
.hos-live__frame {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--ink-900);
  margin-top: var(--space-2);
}

.hos-live__frame iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

@media (max-width: 900px) {
  .hos-live__panel { padding: var(--card-pad); }
  .hos-live__title { font-size: var(--text-h3); }
}

/* ==========================================================================
   Shared page utilities: from the design file's <style> block
   ========================================================================== */

.hos-hosts-stack {
  display: flex;
  flex-direction: column;
  gap: var(--grid-gap);
}

/* SMALL PHONES: --text-h1 STOPS BEING WIDER THAN THE SCREEN.
   Found at 320x568 in WebKit, on /topics, /topics/<slug>, /merch and
   /sponsorship - four pages whose H1 contains the word HOMEOWNERS.

   --text-h1 is clamp(2.5rem, 4.6vw, 4rem), so on any phone the vw term is far
   below the floor and it resolves to a flat 40px. "HOMEOWNERS" set at 40px in
   Archivo Black measures 316px. A 320px viewport leaves 272px inside the
   gutters. A single word cannot be broken, so it hung 44px off the side of the
   document and put a horizontal scrollbar on all four pages.

   THE TOKEN IS OVERRIDDEN RATHER THAN THE HEADINGS. Every one of those H1s
   carries an inline `font-size: var(--text-h1)` ported from the design file,
   and an inline style beats any stylesheet rule that is not !important. Moving
   the value the variable resolves to reaches all of them without a single
   !important and without touching a page. It is here rather than in
   tokens/typography.css because that file is the design system's set as
   delivered; this is a fix for a viewport the design never specified.

   The floor lands at 28px, where HOMEOWNERS is about 221px and fits with room.
   It lifts at 381px, so every phone the design was actually drawn for keeps
   the 40px heading exactly as it was. */
@media (max-width: 380px) {
  :root { --text-h1: clamp(1.75rem, 8.5vw, 2.5rem); }
}

/* And the safety net, for the next long word rather than this one: a heading
   that still cannot fit breaks instead of widening the document. Nothing on
   the site hits this today - it is here so that a future title, a new topic
   hub, or a longer show name can never reintroduce a page-wide sideways
   scroll, which is a fault nobody notices until it is on a phone. */
h1, h2, h3, h4 { overflow-wrap: break-word; }

/* THE HOME HERO'S H1. A class rather than the inline font-size the other
   ported hero headings carry, because this one has to change at a breakpoint
   and an inline style cannot be overridden without !important.

   --text-h1 clamps at a 40px FLOOR, and 'WELCOME TO THE HOMEOWNERS SHOW
   PODCAST' set at 40px in Archivo Black takes FIVE lines inside the 342px a
   390px phone leaves in the gutters. That is the entire first screen spent on
   the heading: the lede under it, which is the line that actually sells the
   show, never appeared above the fold. The name has to lead - that is the
   point of the change - but leading is not the same as filling.

   The step to --text-h2 (30px floor) sets it in three lines and brings the
   lede and the opening of the intro back onto the first screen. It is the same
   one-step-down move .hos-newsletter__copy h2 already makes on a phone, and it
   lifts at 640 where the wide line has room again. */
.hos-home-hero__title { font-size: var(--text-h1); }

@media (max-width: 640px) {
  .hos-home-hero__title { font-size: var(--text-h2); }
}

/* Drops a word out of a label on a phone, where the full label would wrap.
   Used on the hero's primary button, whose 'Listen to the latest episode' set
   on two lines inside the pill at 390px. It hides a TRAILING part of a real
   sentence rather than swapping in a second copy of the label, so there is
   only ever one version of the string in the document. Whatever it wraps is
   gone from the accessible name on a phone too, so it goes around words the
   label can lose and still say what the control does. */
@media (max-width: 640px) {
  .hos-phone-hide { display: none; }
}

@media (max-width: 900px) {
  .hos-grid-3,
  .hos-grid-2,
  .hos-detail-grid,
  .hos-contact-grid,
  .hos-merch-grid,
  .hos-hero-grid { grid-template-columns: 1fr !important; }
  .hos-detail-aside { position: static !important; }
  /* The design's phone view drops the hero's logo panel entirely. !important
     for the same reason the rules above carry it: the element's own display
     comes from an inline style ported from the design file. */
  .hos-hero-art { display: none !important; }
  :root { --section-y: var(--section-y-mobile); }
}

/* ==========================================================================
   Topic hub index (/topics): the card grid
   ========================================================================== */

/* One anchor per card, so the whole tile is the target and there is exactly one
   link per destination in the document. Laid out as a column so the CTA can be
   pushed to the bottom edge and every card in a row ends on the same line no
   matter how long its hook runs. */
.hos-topic-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--card-pad);
  background: var(--surface-card);
  border: var(--border-1) solid var(--border-hairline);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
  text-decoration: none;
  color: inherit;
  transition: box-shadow .18s ease, transform .18s ease, border-color .18s ease;
}

.hos-topic-card:hover {
  border-color: var(--accent);
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-2px);
}

.hos-topic-card__count { color: var(--accent-press); }

.hos-topic-card__name {
  font-family: var(--font-display);
  font-size: var(--text-h4);
  line-height: var(--leading-heading);
  letter-spacing: var(--tracking-tight);
  color: var(--text-display);
}

.hos-topic-card__hook {
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  color: var(--text-body);
}

/* margin-top:auto pins this to the bottom of the card regardless of hook
   length, which is the whole reason the card is a flex column. */
.hos-topic-card__more {
  margin-top: auto;
  padding-top: var(--space-2);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--accent-press);
}

.hos-topic-card__more .hos-icon { width: 15px; height: 15px; }

/* The 900px breakpoint at the top of this block collapses .hos-grid-3 to a
   single column, which is right for three wide episode cards and wrong for
   twelve short topic tiles - one column of twelve is a very long page. Two
   columns is the middle step, and the shared rule still takes over below
   560px. */
@media (max-width: 900px) and (min-width: 561px) {
  .hos-topic-grid { grid-template-columns: repeat(2, 1fr) !important; }
}

/* ==========================================================================
   TOUCH TAP TARGETS

   Everything in this block exists for one reason: on a touch screen a control
   smaller than 44x44 CSS pixels is a control the visitor misses. The number is
   --tap-min (44px), already a token, already what .hos-field and .hos-btn--md
   are built to.

   IT IS ONE SECTION RATHER THAN A RULE IN EACH COMPONENT BLOCK ABOVE, against
   this file's usual organise-by-component convention, because "is every
   control on this site thumb-sized" is a question somebody has to be able to
   answer by reading ONE place. Scattered across nine component blocks it is a
   property nobody can audit and everybody re-breaks. It sits last in the file
   so it wins ties on source order against the component rules it widens.

   SCOPED TO @media (pointer: coarse) - a touch screen, not a narrow window.
   A mouse is already pixel-accurate, so the desktop rendering of every one of
   these components is byte-for-byte what it was; and a phone gets the bigger
   target even in landscape, where a width breakpoint would have missed it.

   The techniques, in order of preference:
     1. min-height / min-width on a control that can simply be bigger.
     2. An ::after overlay for a control whose SIZE IS THE DESIGN (the 6px seek
        track, the 32px chips) - the visual box is untouched and only the hit
        area grows. Pseudo-elements are not event targets, so the click still
        reports the host element and every existing handler is unchanged.
     3. Inline padding-block for text spans, which expands the hit box into the
        leading without moving a single line of text.
   ========================================================================== */

@media (pointer: coarse) {

  /* ---- Footer: every link in the columns, the RSS row, the mailto ------- */
  .hos-footer__link,
  .hos-footer__email {
    display: flex;
    align-items: center;
    min-height: var(--tap-min);
  }

  /* ---- "Show notes" on a card, and the featured player's own notes link -- */
  .hos-episode-card__notes,
  .hos-featured-player__notes {
    min-height: var(--tap-min);
    align-items: center;
  }

  /* ---- The seek track ---------------------------------------------------
     6px tall by design, and the design is right: a fat bar would dominate the
     transport. So the BAR keeps its 6px and an invisible ::after gives it a
     44px hit area, centred, full width. Both onSeek() handlers (js/pages/
     home.js and js/pages/episode.js) already clamp the ratio to [0,1] before
     seeking, so a thumb landing above, below or just off either end of the
     track resolves to a sane position instead of a negative one. */
  .hos-featured-player__track { position: relative; }

  .hos-featured-player__track::after {
    content: "";
    position: absolute;
    inset: calc((var(--tap-min) - 6px) / -2) 0;
  }

  /* A disabled track is a read-only meter, so it gets no hit area to promise
     a seek it will refuse. */
  .hos-featured-player__track[aria-disabled="true"]::after { content: none; }

  /* ---- Topic chips and their hub anchors --------------------------------
     The 32px pill is the design's, and at 44px the fourteen-chip row on
     /episodes turns into a wall. So the pill stays 32px and grows a hit area
     the same way the seek track does. The row's gap is --space-2 (8px), which
     with a 44px target either side would overlap; the row therefore opens its
     ROW gap to 12px here so the vertical pitch is 32 + 12 = 44 and two rows of
     chips never fight over the same pixels. Horizontally the ::after is inset
     to the pill's own width, so neighbouring chips cannot steal each other's
     taps either - the width of a chip is its label, and every label but the
     hub arrow is already well past 44px.

     THE ARROW ZONE IS THE EXCEPTION. It is icon-only and 40px wide, which is
     under 44. WCAG 2.5.8 allows that: the exception is target SPACING, and this
     target sits inside a capsule whose other zone is its own 44px-tall target
     and whose nearest neighbour is a full --space-2 away with no target of its
     own in between. 40px also buys the thing the owner actually asked for - a
     capsule that reads as one chip rather than a chip with a slab bolted to it.
     Held at 44 the arrow would be wider than the word DIY beside it.

     Inside a capsule the zones are 30px tall, not 32 (the capsule's own hairline
     takes 1px top and bottom), so their overlay is inset 7px rather than 6 and
     the hit box is the same 44. */
  .hos-chip-row { row-gap: var(--space-3); }

  .hos-tag {
    position: relative;
    min-width: var(--tap-min);
    justify-content: center;
  }

  .hos-tag::after {
    content: "";
    position: absolute;
    inset: calc((var(--tap-min) - 32px) / -2) 0;
  }

  .hos-chip__hub {
    position: relative;
    min-width: 40px;
  }

  .hos-chip .hos-tag::after,
  .hos-chip__hub::after {
    content: "";
    position: absolute;
    inset: calc((var(--tap-min) - 30px) / -2) 0;
  }

  /* ---- Transcript: the timestamp button and the cue spans ---------------
     The timestamp is a real button and can simply be taller. The cue is an
     inline span inside a paragraph, and its hit box is its 18px content box
     while the line box around it is ~30px at --leading-long. padding-block on
     an inline element does NOT change line height or reflow the paragraph, so
     5px top and bottom hands the thumb the leading that was already there and
     the transcript sets exactly as it did. The active-cue tint grows with it,
     which on a phone reads as a highlight rather than a tight underline. */
  .hos-transcript__time {
    min-height: var(--tap-min);
    padding-inline: var(--space-2);
    margin-inline-start: calc(var(--space-2) * -1);
  }

  .hos-transcript__cue { padding-block: 5px; }

  /* ---- The seek-correction chip's dismiss button ------------------------ */
  .hos-seekchip__close {
    min-height: var(--tap-min);
    padding-inline: var(--space-2);
  }
}

/* =========================================================================
   THE VIDEO FACADE (episode pages, the 174 with a mapped YouTube upload)
   =========================================================================

   WHAT IT IS AND WHY IT LOOKS SECONDARY ON PURPOSE.

   partials/episode_lib.php renders this strip directly under the hero's
   FeaturedPlayer; js/video-facade.js swaps it for the real iframe on click.
   See hos_episode_video_facade_markup() for why the page needs it at all - it
   is the fix for a VideoObject that was being published on pages with no
   player in them.

   The strip has to complement an audio-first hero rather than compete with it,
   and every choice below is that:

     - it shares .hos-featured-player's surface, border and radius, so the two
       read as one hero block rather than a widget bolted underneath one
     - its artwork is capped at 132px against the player's 260px, and its type
       runs at --text-sm against the player's --text-h1. Same family, smaller
       voice
     - the play badge is a SQUARE-ish rounded rect, deliberately NOT the pill
       the audio play button is. Two round accent buttons in one hero, on the
       same picture, doing different things is the confusion this placement
       exists to avoid; a different shape is the cheapest possible way to say
       "this is the other one"
     - the accent is on the badge only. The strip's own chrome stays neutral so
       the orange in this hero still means "play the audio" first

   It is a <button> containing a grid, so `display: grid` sits on the button
   itself and the wrapper does nothing but hold the data attributes and the
   frame box after the swap. */

.hos-video-facade {
  /* No styling of its own before the swap - the button is the whole visible
     component. After the swap this element is what the 16:9 frame lives in. */
  display: block;
}

.hos-video-facade__btn {
  display: grid;
  grid-template-columns: minmax(96px, 132px) 1fr;
  gap: var(--space-5);
  align-items: center;
  width: 100%;
  /* Not --card-pad-lg: this is the smaller sibling of the player above it. */
  padding: var(--space-4);
  text-align: left;
  cursor: pointer;
  background: var(--surface-inverse-card);
  border: var(--border-1) solid var(--border-inverse);
  border-radius: var(--radius-lg);
  color: inherit;
  font: inherit;
  transition: var(--transition-interactive);
}

.hos-video-facade__btn:hover {
  border-color: var(--accent);
  background: var(--surface-inverse-raised);
}

/* The same focus ring every other control on the site uses. A facade that
   cannot be reached by keyboard is a video 174 pages cannot play. */
.hos-video-facade__btn:focus-visible {
  outline: none;
  box-shadow: var(--ring-focus);
}

.hos-video-facade__btn:active { transform: scale(var(--press-scale)); }

.hos-video-facade__art {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--ink-900);
}

/* .hos-card-art--thumb rides on this element too and supplies object-fit:cover,
   which is what crops hqdefault's 4:3 letterboxing back to the video's real
   16:9 frame - the same trick every card on the site uses. */
.hos-video-facade__art img {
  width: 100%;
  height: 100%;
  display: block;
}

/* THE BADGE. Rounded rect, not a pill: see the block comment above. Sized in
   absolute px rather than tokens because it is an overlay on a fixed-ratio box
   and has no type to align to. */
.hos-video-facade__badge {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 44px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: var(--radius-md);
  background: var(--accent);
  color: var(--text-on-accent);
  box-shadow: var(--shadow-sm);
  transition: var(--transition-interactive);
}

.hos-video-facade__badge svg {
  width: 16px;
  height: 16px;
  /* The play triangle is a stroked polygon at 1em elsewhere; filled here so it
     reads as solid at this size against the accent. */
  fill: currentColor;
}

.hos-video-facade__btn:hover .hos-video-facade__badge { background: var(--accent-hover); }

.hos-video-facade__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  min-width: 0;
}

.hos-video-facade__eyebrow { color: var(--accent); }

.hos-video-facade__title {
  font-size: var(--text-base);
  font-weight: var(--weight-bold);
  color: var(--white);
}

/* The consequence, said before the visitor discovers it by hearing two things
   at once. js/video-facade.js is what makes the sentence true. */
.hos-video-facade__note {
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  color: var(--gray-400);
}

/* ---- After the swap --------------------------------------------------- */

/* The iframe has no intrinsic size worth having, so the ratio is enforced on
   the box and the frame fills it. --radius-lg matches the button it replaced,
   so the hero does not change shape when the video loads. */
.hos-video-facade__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--ink-900);
  border: var(--border-1) solid var(--border-inverse);
}

.hos-video-facade__frame iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* Phone: the strip stacks. At 390px a 96px thumbnail beside three lines of type
   leaves the note wrapping to four lines in a 180px column, which reads as a
   paragraph rather than a caption. Full-width art above the label is the same
   shape the episode cards take at this width. */
@media (max-width: 640px) {
  .hos-video-facade__btn {
    grid-template-columns: 1fr;
    gap: var(--space-4);
  }

  .hos-video-facade__art { max-width: none; }

  /* The badge is sized against the ARTWORK, not against the viewport, and the
     artwork just went from 132px wide to the full column. At 44px it read as a
     sticker dropped on a photograph rather than as the control in the middle of
     it. It also clears --tap-min in both axes here, which the desktop size does
     not need to: on a pointer device the whole strip is the target, but a thumb
     aims at the badge. */
  .hos-video-facade__badge {
    width: 64px;
    height: 46px;
  }

  .hos-video-facade__badge svg {
    width: 22px;
    height: 22px;
  }
}
