/* The listening section. Borrows the page's existing vocabulary rather than
   introducing a second one: h1 is the small coral label, h2 is 21px/weight 100,
   the column is 600px capped at 80%. No cards, no shadows, no new typeface. */

/* The room is a centred column at every width. The About page hugs the left
   because the right half belongs to the photograph; nothing ever fills that
   space here, so a left-hugging column under a full-width header just looked
   unbalanced. width:100% rather than 80% so it agrees with the footer instead
   of landing 24px narrower. */
/* Wide enough for the two-column room: 600 for the list, 300 for the pinned
   card, 56 between. At 600 the grid had nowhere to put the second column and
   silently collapsed to one. */
.listening,
[data-listening] {
	max-width: 956px;
	width: 100%;
	margin-left: auto;
	margin-right: auto;
}


/* The anchor: one track Edwin chose, and why. This is the authored half of the
   section, so it gets the room the recent list does not. */


/* now playing / last played */


.now-art { width: 56px; height: 56px; }
.row-art { width: 34px; height: 34px; }

.now-body { min-width: 0; }

.now-title {
	font-size: 21px;
	font-weight: 100;
	line-height: 1.35;
	margin: 0;
}

.meta {
	font-size: 12px;
	font-weight: 400;
	color: var(--muted);
	margin: 3px 0 0;
}

/* The live dot only appears when the claim is actually true — see js/listening
   /lately.js, which requires both the nowplaying flag and a fresh response. */
.live-dot {
	display: inline-block;
	width: 6px;
	height: 6px;
	border-radius: 50%;
	background: var(--accent);
	margin-right: 6px;
	vertical-align: middle;
}




.row-open:hover .row-title { color: var(--accent); }

.row-body { min-width: 0; flex: 1; }

.row-title {
	/* display:block is what makes the ellipsis work at all -- overflow has no
	   effect on an inline box, so without it a long title pushes the timestamp
	   off the right edge and the page scrolls sideways on a phone. */
	display: block;
	font-size: 15px;
	font-weight: 300;
	line-height: 1.3;
	transition: color .2s ease-in-out;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.row-when {
	font-size: 12px;
	color: var(--muted);
	flex: none;
	white-space: nowrap;
}



.links { font-size: 12px; color: var(--muted); }
.links a { border-bottom: 1px solid var(--line); }
.links a:hover { border-bottom: 3px solid var(--accent); }
.links span[aria-hidden] { padding: 0 6px; }

.listening-empty {
	font-size: 15px;
	font-weight: 300;
	color: var(--muted);
	margin: 0;
}


@media (max-width: 768px) {
	.listening,
	.listening-head { width: 100%; }
	.row-detail { padding-left: 0; }
}

/* ── The player bar ───────────────────────────────────────────────────────── */

/* Mounted once on every page and only visible while something is playing. An
   empty transport sitting under a page about payments infrastructure would be
   furniture, so it hides itself rather than idling. */
.player {
	position: fixed;
	inset: auto 0 0 0;
	z-index: 40;
	background: color-mix(in srgb, var(--ground) 94%, transparent);
	backdrop-filter: blur(10px);
	border-top: 1px solid var(--line);
}

.player[hidden] { display: none; }

.player-inner {
	display: flex;
	align-items: center;
	gap: 12px;
	max-width: 900px;
	margin: 0 auto;
	padding: 8px 16px;
}

.player-art {
	width: 40px;
	height: 40px;
	flex: none;
	border-radius: 2px;
	object-fit: cover;
	background: var(--line-soft);
}

.player-body { min-width: 0; flex: 1 1 160px; }

.player-title {
	font-size: 13px;
	font-weight: 300;
	margin: 0;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.player-meta { font-size: 11px; color: var(--muted); margin: 2px 0 0; }

.player-transport { display: flex; align-items: center; gap: 8px; flex: none; }

.player-btn {
	font: inherit;
	font-size: 11px;
	color: var(--muted);
	background: none;
	border: 0;
	padding: 4px;
	cursor: pointer;
	line-height: 1;
}

.player-btn:hover { color: var(--ink); }

.player-play {
	width: 30px;
	height: 30px;
	flex: none;
	border: 0;
	border-radius: 50%;
	background: var(--accent);
	color: var(--ground);
	font-size: 11px;
	line-height: 1;
	cursor: pointer;
	transition: transform .15s ease-in-out;
}

.player-play:hover { transform: scale(1.06); }

/* A div rather than <input type=range>: pointer capture means a drag that
   leaves the bar still seeks, and the fill needs no vendor pseudo-elements. */
.player-seek {
	flex: 1 1 140px;
	min-width: 80px;
	padding: 8px 0;
	cursor: pointer;
	touch-action: none;
}

.player-track {
	height: 3px;
	border-radius: 999px;
	background: color-mix(in srgb, var(--ink) 12%, transparent);
	overflow: hidden;
}

.player-fill {
	height: 100%;
	width: 0;
	background: var(--accent);
}

.player-time {
	font-size: 11px;
	color: var(--muted);
	font-variant-numeric: tabular-nums;
	flex: none;
}



@media (max-width: 768px) {
	.player-time,
	.player-art { display: none; }
	.player-inner { gap: 8px; padding: 8px 12px; }
}

@media (prefers-reduced-motion: reduce) {
	.player-play { transition: none; }
	.player-play:hover { transform: none; }
}

/* Room for the bar. Only while it is up, so a page with nothing playing keeps
   its original spacing exactly. */
/* Reserve room for the bar so the footer is reachable while it is up. The
   bar is fixed and overlays the page, so this is scroll room rather than a
   push -- the last row of links sits under it until you scroll. */
/* Reserved ALWAYS, not only while the bar is up. Adding it on mount made the
   page grow by the bar's height the moment you pressed play, which turned a
   page that fitted into one that scrolled. The footprint is constant now, so
   mounting the bar changes no measurement -- and the footer links stop hiding
   behind it on a phone. */
/* 88, not 120: the bar measures 57, and the rest is breathing room. Every
   pixel reserved here is a pixel the room cannot use above the fold. */
.content {
	padding-bottom: 88px;
}




/* ── The corner on the About page ─────────────────────────────────────────── */

.corner-play {
	font: inherit;
	color: inherit;
	background: none;
	border: 0;
	border-bottom: 3px solid transparent;
	padding: 0;
	cursor: pointer;
	display: inline-flex;
	align-items: baseline;
	gap: 6px;
}

.corner-play:hover { border-bottom-color: var(--accent); }
.corner-icon { font-size: 9px; color: var(--accent); }

/* ── Marquee ──────────────────────────────────────────────────────────────── */

/* Park, drift to reveal the tail, park again, return. Ported from Onus; a
   continuous loop is harder to read and never shows you the beginning. The
   distance is measured in JS and arrives as --marquee-shift. */
.marquee {
	display: block;
	overflow: hidden;
	position: relative;
	max-width: 100%;
}

.marquee-inner {
	display: inline-block;
	white-space: nowrap;
	will-change: transform;
}

.marquee.is-drifting .marquee-inner {
	animation-name: eh-marquee;
	animation-timing-function: ease-in-out;
	animation-iteration-count: infinite;
}

@keyframes eh-marquee {
	0%, 18%   { transform: translateX(0); }
	47%, 65%  { transform: translateX(var(--marquee-shift, 0)); }
	94%, 100% { transform: translateX(0); }
}

/* ── The player bar ───────────────────────────────────────────────────────── */

.player-links { font-size: 11px; flex: none; }
.player-links .links { margin: 0; }

@media (prefers-reduced-motion: reduce) {
	.marquee.is-drifting .marquee-inner { animation: none; }
	.play-all { transition: none; }
	.play-all:hover { transform: none; }
}

/* Narrow: the transport drops to its own row rather than fighting the title
   for width, and every control keeps a 44px touch target. */
@media (max-width: 640px) {
	.player-inner {
		display: grid;
		grid-template-columns: auto 1fr auto;
		grid-template-areas:
			"art  title close"
			"seek seek  seek"
			"tx   tx    tx";
		gap: 6px 10px;
		padding: 8px 12px 10px;
	}

	.player-art { grid-area: art; display: block; width: 34px; height: 34px; }
	.player-body { grid-area: title; }
	.player-close { grid-area: close; }
	.player-seek { grid-area: seek; width: 100%; }
	.player-transport { grid-area: tx; justify-content: center; gap: 22px; }
	.player-links { display: none; }
	.player-time { display: none; }

	.player-btn { min-width: 44px; min-height: 44px; font-size: 13px; }
	.player-play { width: 44px; height: 44px; font-size: 13px; }
}

/* The links were wrapping to a second line and making the bar half again as
   tall, which is how it ended up covering the footer. One line, truncated, and
   only where there is genuinely room for them. */
.player-links {
	max-width: 260px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.player-links .links {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

@media (max-width: 1200px) {
	.player-links { display: none; }
}

/* The corner is right-aligned, which put the marquee's overflow on the LEFT --
   so at rest it parked showing the tail of the title with the artist's name
   already scrolled off. The label fills the box either way once it overflows,
   so aligning this one line left costs nothing visually and means it starts
   at the beginning. */
.corner-now .marquee {
	text-align: left;
}

/* ── The corner line ──────────────────────────────────────────────────────── */

/* One row under the label, matching Elsewhere and Portfolio beside it: the
   track title first, then the two things you can do. Title first because it is
   the part that changes -- the controls are always the same two words.

   Right-anchored, flush with the edge "Lounge" already sits on. */
.corner-line {
	display: flex;
	justify-content: flex-end;
	align-items: baseline;
	gap: 7px;
	white-space: nowrap;
	/* 12px, not 0, to match what the UA gives the Elsewhere and Portfolio
	   paragraphs beside it. `margin: 0` left this line sitting 4px above them --
	   the label row aligned, so only the line under it looked wrong, which is
	   exactly the kind of misalignment that reads as sloppy without being
	   obvious. */
	margin: 12px 0 0;
}

/* The only flexible item: it gives up width first and clips, so the controls
   are never pushed off the edge by a long title. */
.corner-now {
	flex: 0 1 auto;
	min-width: 0;
	font-size: 12px;
	color: var(--muted);
}

.corner-now[hidden] { display: none; }
.corner-sep { color: var(--muted); }

@media only screen and (max-width: 768px) {
	/* Stacked footer: the title gets its own line rather than squeezing the
	   controls down to nothing. */
	.corner-line { flex-wrap: wrap; justify-content: flex-start; }
	.corner-now { flex-basis: 100%; }
}

/* ── The room ─────────────────────────────────────────────────────────────── */

/* Two columns on desktop. Moving the pinned card out of the vertical flow is
   what buys the room to fit above the fold, and it fills a right half that was
   otherwise empty.

   The chosen thing reads first: the pinned song on the left, the log of what
   actually played on the right. Left is what Edwin picked, right is what
   happened -- a split the queued-songs section will extend rather than
   disturb. */
.room {
	display: grid;
	grid-template-columns: minmax(220px, 300px) minmax(0, 600px);
	gap: 56px;
	align-items: start;
	/* The site's vertical rhythm is 50px -- h1 and .footer-row both use it. The
	   room lost it because .room-head h1 has to kill the inherited margin-top to
	   sit beside the Play button, which left the title row 8px above the list. */
	margin-top: 50px;
}

.room-main { min-width: 0; }

/* Ordered rather than reordered in the DOM, because the narrow layout wants the
   same thing -- chosen first, then the list -- and one declaration serves both.
   Swapping the markup instead would invert the narrow layout and need a
   counter-override to undo it.

   The order sits on the column, not on the card: .pin shares that column with
   the visitors' queue now, and the two travel together. */
.room-side {
	order: -1;
	min-width: 0;
	display: flex;
	flex-direction: column;
	gap: 30px;
}

/* Both columns open with a header row of the SAME height, which is the whole
   reason the two labels line up. Anything else needs an offset computed from
   the Play button's height, and the moment the button changes the offset is
   wrong -- which is exactly how the labels came to be 26px apart. */
.room-head,
.pin-head {
	display: flex;
	/* flex-start, not center. Centred, the 42px Play button in a 44px row began
	   at 208 while the labels beside it began at 221 -- so the control sat above
	   the thing it belongs to, and read as taking precedence over the section
	   labels. Aligned to the top, nothing rises above a label. The min-height
	   stays: it is what keeps .pin-head and .room-head equal so the two columns
	   start their content on the same line. */
	align-items: flex-start;
	justify-content: space-between;
	gap: 20px;
	min-height: 44px;
	margin-bottom: 18px;
}

/* The label is the top of the section, and the action hangs below its line --
   not level with it, so there is no ambiguity about which of the two is the
   heading. Two pixels is enough to read as deliberate without costing height. */
.room-head .play-all,
.room-head .q-open { margin-top: 3px; }

/* One label, two columns. Left is an h1 and right is a p, so without this they
   inherit different line-heights and different widths and sit on different
   baselines even once the rows match. */
.room-head h1,
.pin-label {
	margin: 0;
	width: auto;
	max-width: none;
	font-size: 12px;
	line-height: 1.4;
	font-weight: 400;
	color: var(--accent);
	letter-spacing: 0.04em;
}

/* Spotify-sized, because it is the one control the page wants you to press. */
.play-all {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 9px;
	flex: none;
	font: inherit;
	font-size: 13px;
	color: var(--ground);
	background: var(--accent);
	border: 0;
	border-radius: 999px;
	padding: 11px 22px;
	cursor: pointer;
	transition: transform .15s ease-in-out;
}

.play-all:hover { transform: scale(1.04); }
/* [hidden] loses to a class selector's display, so this has to be said. */
.play-all[hidden] { display: none; }
.play-all-icon { display: flex; }

/* ── Track rows ───────────────────────────────────────────────────────────── */

/* Sized off the viewport, and still the reason the RIGHT column lands above the
   fold. The left column no longer does, and cannot: it carries the pinned song,
   the visitors' queue and the top charts, and the queue's height belongs to
   whoever leaves songs. Trimming the charts to protect a one-screen promise
   that visitors can break anyway would be mutilating the feature to preserve
   an illusion -- so the page scrolls a little, and the list still does not. */
.trk-window {
	/* Tuned so the two columns end near level and the room fits the window. The
	   left column's height is fixed by the pinned card plus the top charts; this
	   is the only dial on the right, since the queue below it has its own. */
	max-height: calc(100vh - 639px);
	min-height: 150px;
	overflow-y: auto;
	overscroll-behavior: contain;
	-webkit-overflow-scrolling: touch;
	/* Clears the scrollbar. Without it the relative times sat flush against it
	   with about three pixels between the two. */
	padding-right: 12px;
	/* The window lands mid-row, which is the honest signal that there is more.
	   Cut hard it read as clipped; faded it reads as continuing. */
	mask-image: linear-gradient(to bottom, #000 calc(100% - 24px), transparent);
}

.trk-list { list-style: none; margin: 0; padding: 0; }

.trk { border-bottom: 1px solid var(--line-soft); }

/* Every column here is a fixed track, and that is the point: each row is its
   OWN grid, so any track sized from content resolves to a different width in
   every row. With `auto` on the time and `minmax(0, 120px)` on the album, "5
   minutes ago" and "2 hours ago" pushed their neighbours to different x
   positions and the two middle columns read as drifting rather than as
   columns. Fixed tracks make every row agree. 84px fits the longest relative
   time the formatter produces. */
.trk-btn {
	display: grid;
	grid-template-columns: 22px 36px minmax(0, 1fr) 120px 84px;
	align-items: center;
	gap: 12px;
	width: 100%;
	padding: 8px 0;
	background: none;
	border: 0;
	font: inherit;
	color: inherit;
	text-align: left;
	cursor: pointer;
}

.trk-btn:hover { background: color-mix(in srgb, var(--ink) 4%, transparent); }

/* The number becomes the play control on hover -- the affordance appears where
   the eye already is, rather than in a separate column that is empty until you
   need it. */
.trk-num { position: relative; display: block; text-align: right; }
.trk-n { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.trk-play { display: none; justify-content: flex-end; color: var(--accent); }
.trk-btn:hover .trk-n,
.trk.is-playing .trk-n { display: none; }
.trk-btn:hover .trk-play,
.trk.is-playing .trk-play { display: flex; }

.trk-art {
	width: 36px;
	height: 36px;
	border-radius: 2px;
	object-fit: cover;
	background: var(--line-soft);
	display: block;
}

.trk-main { min-width: 0; display: block; }

.trk-title {
	display: block;
	font-size: 14px;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.trk-artist {
	display: block;
	font-size: 12px;
	color: var(--muted);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.trk-album {
	font-size: 12px;
	color: var(--muted);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.trk-when {
	font-size: 11px;
	color: var(--muted);
	white-space: nowrap;
	text-align: right;
}

.trk.is-playing .trk-title { color: var(--accent); }

/* ── The pinned card ──────────────────────────────────────────────────────── */

.pin[hidden] { display: none; }

/* Set as a quote, because it is the one sentence on the page in Edwin's own
   voice and nothing else marked it as his. */
.pin-note {
	font-size: 21px;
	font-weight: 100;
	line-height: 1.35;
	margin: 0 0 18px;
}

/* The quotation marks belong to the QUOTE, not to the blockquote. When they
   were on the blockquote the signature -- a child of it -- fell inside them,
   and the closing mark rendered on its own line underneath, which is how a
   stray " ended up floating below "-- Edwin". */
.pin-quote { quotes: '\201C' '\201D'; }
.pin-quote::before { content: open-quote; }
.pin-quote::after { content: close-quote; }

.pin-track {
	display: grid;
	grid-template-columns: 56px minmax(0, 1fr) 20px;
	align-items: center;
	gap: 12px;
	width: 100%;
	background: none;
	border: 0;
	padding: 0;
	font: inherit;
	color: inherit;
	text-align: left;
	cursor: pointer;
}

.pin-art {
	width: 56px;
	height: 56px;
	border-radius: 2px;
	object-fit: cover;
	background: var(--line-soft);
	display: block;
}

.pin-meta { min-width: 0; display: block; }
.pin-title { display: block; font-size: 14px; line-height: 1.3; }
.pin-artist { display: block; font-size: 12px; color: var(--muted); }
.pin-since { display: block; font-size: 11px; color: var(--muted); margin-top: 3px; }
.pin-state { color: var(--accent); display: flex; justify-content: flex-end; }
.pin-track:hover .pin-title,
.pin-track.is-playing .pin-title { color: var(--accent); }

/* ── Live ─────────────────────────────────────────────────────────────────── */

.now-word { color: var(--accent); }

.live-dot.is-live {
	animation: eh-pulse 2s ease-in-out infinite;
}

@keyframes eh-pulse {
	0%, 100% { opacity: 1; }
	50% { opacity: .35; }
}

@media (prefers-reduced-motion: reduce) {
	.live-dot.is-live { animation: none; }
	.play-all { transition: none; }
	.play-all:hover { transform: none; }

	/* The belt stops, and the viewport becomes scrollable instead -- the line is
	   still there to be read, it just no longer moves on its own. */
	.ticker { animation: none; }
	.ticker-belt { animation: none; }
	.ticker-viewport { overflow-x: auto; }
	.ticker-belt .ticker-run:nth-child(2) { display: none; }
}

/* ── Narrow ───────────────────────────────────────────────────────────────── */

@media only screen and (max-width: 1100px) {
	/* One column. The pinned song still leads -- .pin's order is set in the base
	   rule now, so this only has to collapse the columns. */
	.room { grid-template-columns: minmax(0, 1fr); gap: 30px; }
}

@media only screen and (max-width: 640px) {
	/* A fixed last column, not auto: the queue's cell says "CC 11m" where the
	   recent list's says "2 hours ago", and auto sized each to its own content
	   -- which moved the title column and broke the alignment between two lists
	   that are supposed to read as one. */
	.trk-btn { grid-template-columns: 20px 34px minmax(0, 1fr) 68px; gap: 10px; }
	.trk-album { display: none; }
	.trk-window { max-height: calc(100vh - 454px); }
	.pin-note { font-size: 18px; }

	/* The bar is three rows on a phone -- artwork and title, seek, transport --
	   and measures 138px against the 57 it takes on desktop. Reserving the
	   desktop figure left the footer links tucked underneath it. */
	.content { padding-bottom: 165px; }
}

/* The note is the one sentence on the page in Edwin's own voice; signing it is
   what makes that legible rather than assumed.

   Inline, and in the same inline formatting context as the quote, so it sits on
   the quote's last line whenever there is room and only wraps when there is
   not. display:block forced it onto a line of its own every time. */
.pin-cite {
	font-size: 12px;
	color: var(--muted);
	margin-left: .5em;
	white-space: nowrap;
}

/* ── Sections ─────────────────────────────────────────────────────────────── */

/* The Lounge holds four things. With only whitespace between them they read as
   one undivided mass -- worst on a phone, where all four stack. The Portfolio
   page already solved this with a 2px --line-strong rule between major blocks;
   same vocabulary rather than a second one. */
.room-block + .room-block {
	margin-top: 30px;
	padding-top: 30px;
	border-top: 2px solid var(--line-strong);
}

/* Collapsed to one column, the four blocks are still two parents' worth of
   siblings, so the adjacent-sibling rule leaves no rule between the last block
   of the left column and the first of the right. Visually they are adjacent. */
@media only screen and (max-width: 1100px) {
	.room-main > .room-block:first-child {
		margin-top: 30px;
		padding-top: 30px;
		border-top: 2px solid var(--line-strong);
	}
}

/* ── Songs visitors have left ─────────────────────────────────────────────── */

/* The same list as the one above it, with one extra column for the signature.
   It shares .trk's grid rather than carrying its own -- three columns against
   the recent list's five made two lists in one column read as two unrelated
   widgets. */
.queue[hidden] { display: none; }

/* Its own scroll, so a full queue cannot push the page off the fold. Same
   treatment .trk-window has, fade included, so a half-row reads as "there is
   more" rather than as clipped. */
.q-window {
	max-height: 116px;
	overflow-y: auto;
	overscroll-behavior: contain;
	-webkit-overflow-scrolling: touch;
	padding-right: 12px;
	mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
}

/* The signature, sharing the time cell. Right-hand side, as asked, without
   costing the list a column the one above it does not have. */
.trk-when .q-sign {
	color: var(--muted-strong);
	letter-spacing: 0.04em;
	margin-right: 6px;
}

.q-count { color: var(--muted); letter-spacing: 0; font-size: 11px; }

/* The one thing this list says that the recent list does not. */
.q-played {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	color: var(--accent);
	white-space: nowrap;
}

.q-tick { font-size: 10px; }
.q-row.is-played .trk-title { color: var(--muted-strong); }

/* An action, not a caption -- so it sits where Play sits on the list above. */
.q-open {
	flex: none;
	font: inherit;
	font-size: 12px;
	color: var(--accent);
	background: none;
	border: 0;
	border-bottom: 1px solid var(--line);
	padding: 0 0 2px;
	cursor: pointer;
}

.q-open:hover { border-bottom-color: var(--accent); }

.q-empty { margin: 0; font-size: 13px; color: var(--muted); width: auto; max-width: none; }

/* The visitor's mark in the log. Says who a song came from, where the queue row
   says that Edwin played it -- the same fact from both ends. */
.trk-from {
	font-size: 11px;
	color: var(--accent);
	white-space: nowrap;
}

/* ── Top artists and tracks ───────────────────────────────────────────────── */

/* Under the queue, in the column that is about Edwin rather than about the last
   few hours -- and filling the space that column had been carrying empty since
   the pinned card moved left. */

.top[hidden] { display: none; }

.top-head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 12px;
	margin-bottom: 8px;
}

.top-label {
	margin: 0;
	width: auto;
	max-width: none;
	font-size: 12px;
	line-height: 1.4;
	color: var(--accent);
	letter-spacing: 0.04em;
}

.top-kinds { display: inline-flex; gap: 2px; flex: none; }

.top-windows { display: flex; flex-wrap: wrap; gap: 2px; margin-bottom: 10px; }

/* Text buttons rather than a segmented control: the page has no other boxed
   widget, and one here would be the loudest thing in the column. */
.top-pill {
	font: inherit;
	font-size: 11px;
	color: var(--muted);
	background: none;
	border: 0;
	border-bottom: 1px solid transparent;
	padding: 2px 4px;
	cursor: pointer;
}

.top-pill:hover { color: var(--ink); }
.top-pill.is-on { color: var(--accent); border-bottom-color: var(--accent); }

/* The same 44px floor the transport and the picker keep -- at the desktop size
   these were 22px tall, which is half a thumb. Vertical padding only, and the
   spacing moves to the container's gap: padding on the sides needed a negative
   margin to keep the first pill flush with the labels above it, and that made
   the row wider than the column it sits in. */
@media (max-width: 1100px) {
	.top-pill { padding: 13px 0; }
	.top-kinds, .top-windows { gap: 14px; }
}

.top-list { list-style: none; margin: 0; padding: 0; }

.top-row { border-bottom: 1px solid var(--line-soft); }
.top-row:last-child { border-bottom: 0; }

.top-btn {
	display: grid;
	grid-template-columns: 22px minmax(0, 1fr) auto;
	align-items: center;
	gap: 10px;
	width: 100%;
	padding: 6px 0;
	background: none;
	border: 0;
	font: inherit;
	color: inherit;
	text-align: left;
	cursor: pointer;
}

a.top-btn { text-decoration: none; }
a.top-btn:hover .top-name,
button.top-btn:hover .top-name { color: var(--accent); }

/* The same swap the recent list uses, so "playable" looks identical wherever it
   appears. Only on button rows: an artist row is an <a> to Last.fm, and a play
   triangle there would promise something the row cannot do. */
.top-rank { position: relative; display: block; text-align: right; }
.top-n { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.top-play { display: none; justify-content: flex-end; color: var(--accent); }
button.top-btn:hover .top-n,
.top-row.is-playing .top-n { display: none; }
button.top-btn:hover .top-play,
.top-row.is-playing .top-play { display: flex; }

/* An artist goes somewhere else, and says so rather than staying silent. */
.top-out { opacity: 0; color: var(--muted); font-size: 11px; margin-left: 4px; }
a.top-btn:hover .top-out { opacity: 1; }

/* A row, not a stack: both kinds are one line so the section's height never
   changes when the toggle does. */
.top-main {
	min-width: 0;
	display: flex;
	align-items: baseline;
	gap: 6px;
	overflow: hidden;
}

/* The artist gives up width first -- the track name is what the chart is
   about, so it keeps its room and the artist ellipses. */
.top-by { flex: 0 1 auto; min-width: 0; }
.top-by::before { content: '\00B7'; margin-right: 6px; color: var(--line-strong); }

.top-name {
	font-size: 13px;
	font-weight: 300;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.top-by {
	font-size: 11px;
	color: var(--muted);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/* Tabular figures so the counts form a column rather than a ragged edge. */
.top-plays {
	font-size: 11px;
	color: var(--muted);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.top-row.is-playing .top-name { color: var(--accent); }

.top-empty { margin: 0; font-size: 13px; color: var(--muted); width: auto; max-width: none; }

/* ── Leaving one ──────────────────────────────────────────────────────────── */

.leave-host[hidden] { display: none; }

.leave {
	display: flex;
	flex-direction: column;
	gap: 12px;
	padding: 16px;
	border: 1px solid var(--line);
	border-radius: 3px;
	background: var(--ground);
}

.leave-lede { margin: 0; font-size: 13px; color: var(--muted); width: auto; max-width: none; }

.leave-field { display: flex; flex-direction: column; gap: 4px; }
.leave-field > span { font-size: 11px; color: var(--accent); letter-spacing: 0.04em; }
.leave-field em { font-style: normal; color: var(--muted); letter-spacing: 0; }

/* 16px, not 14. Safari on iOS zooms the whole page when a field smaller than
   16px takes focus, and it does not zoom back out -- the layout is left
   scrolled sideways with the fixed ticker cropped. text-size-adjust does not
   prevent it; only the font-size does. */
.leave-field input {
	font: inherit;
	font-size: 16px;
	color: var(--ink);
	background: none;
	border: 0;
	border-bottom: 1px solid var(--line);
	padding: 4px 0;
	border-radius: 0;
}

.leave-field input:focus { outline: none; border-bottom-color: var(--accent); }
.leave-initials input { max-width: 5em; text-transform: uppercase; }

/* The results of a search, which is the whole picker now. */
.leave-results { display: flex; flex-direction: column; }
.leave-results:empty { display: none; }

.leave-result {
	display: grid;
	grid-template-columns: 36px minmax(0, 1fr);
	align-items: center;
	gap: 10px;
	width: 100%;
	padding: 7px 0;
	background: none;
	border: 0;
	border-bottom: 1px solid var(--line-soft);
	font: inherit;
	color: inherit;
	text-align: left;
	cursor: pointer;
}

.leave-result:last-child { border-bottom: 0; }
.leave-result:hover .leave-result-track { color: var(--accent); }

.leave-result-art,
.leave-chosen-art {
	width: 36px;
	height: 36px;
	flex: none;
	border-radius: 2px;
	overflow: hidden;
	background: var(--line-soft);
}

.leave-result-art img,
.leave-chosen-art img { display: block; width: 100%; height: 100%; object-fit: cover; }

.leave-result-main,
.leave-chosen-main { min-width: 0; display: flex; flex-direction: column; gap: 1px; }

.leave-result-track,
.leave-chosen-track {
	font-size: 14px;
	font-weight: 300;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.leave-result-artist,
.leave-chosen-artist {
	font-size: 11px;
	color: var(--muted);
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.leave-none { margin: 6px 0 0; font-size: 12px; color: var(--muted); width: auto; max-width: none; }

/* What they picked, held where the results were, so the choice is plainly the
   thing that will be left. */
.leave-chosen {
	display: grid;
	grid-template-columns: 36px minmax(0, 1fr) auto;
	align-items: center;
	gap: 10px;
	padding: 8px;
	border: 1px solid var(--accent);
	border-radius: 3px;
}

.leave-chosen[hidden] { display: none; }

.leave-clear {
	font: inherit;
	font-size: 11px;
	color: var(--muted);
	background: none;
	border: 0;
	padding: 4px;
	cursor: pointer;
}

.leave-clear:hover { color: var(--ink); }

/* The same 44px floor the transport keeps on a phone. */
@media (max-width: 1100px) {
	.leave-result { padding: 10px 0; }
}


.leave-note { margin: 0; font-size: 12px; color: var(--muted); width: auto; max-width: none; min-height: 1.2em; }

.leave-actions { display: flex; align-items: center; justify-content: flex-end; gap: 14px; }

.leave-cancel {
	font: inherit;
	font-size: 12px;
	color: var(--muted);
	background: none;
	border: 0;
	padding: 0;
	cursor: pointer;
}

.leave-cancel:hover { color: var(--ink); }

.leave-send {
	font: inherit;
	font-size: 13px;
	color: var(--ground);
	background: var(--accent);
	border: 0;
	border-radius: 999px;
	padding: 9px 18px;
	cursor: pointer;
}

.leave-send[disabled] { opacity: .6; cursor: default; }

/* ── The ticker (phones) ──────────────────────────────────────────────────── */

/* Pinned to the top of the viewport, edge to edge, and always moving. It began
   as a static line inside .content, which put it 84px down the page -- far
   enough in to compete with the bio while being too small to win. A ticker is
   chrome: it belongs above the page, not inside it.

   Opaque, because the page scrolls underneath. Below the photo modal and the
   player bar by design -- it is the least urgent thing on the screen. */
[data-ticker] { display: none; }
[data-ticker]:not([hidden]) { display: block; }

/* The Lounge never shows one. Said in CSS rather than in JS because the host
   sits at <body> level and survives the swap, so a JS hide that waits on
   /api/lately left the previous page's ticker visibly sitting on the Lounge
   for over a second. A body class is set synchronously by js/nav.js, so this
   applies in the first frame after the swap. */
.page-lounge [data-ticker] { display: none; }

.ticker {
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	z-index: 40;
	display: block;
	height: 34px;
	background: var(--ground);
	border-bottom: 1px solid var(--line);
	font-size: 12px;
	overflow: hidden;
	/* Read back by js/listening/ticker.js to set the belt's duration, so the two
	   breakpoints can run at different speeds off one measurement. */
	--ticker-speed: 45;
	/* The room it occupies is reserved from the first frame; this is only the
	   ticker arriving into it, and a fade reads as intentional where a pop
	   reads as something having gone wrong. */
	animation: eh-fade .16s ease-out;
}

/* The page is absolutely positioned against #particles-js, so it knows nothing
   about a fixed bar above it and has to be told.

   Reserved from the PAGE, not from whether the ticker has loaded. Keyed on
   body.has-ticker it was applied only after /api/lately returned, so every page
   sat 35px too high for 1.3-1.7 seconds and then jumped -- measured at t=1698ms
   on Portfolio and t=1276ms on About -- and entering the Lounge it jumped back
   the other way. The body class comes from the HTML and js/nav.js copies it
   synchronously, so this is settled before the first frame and nothing moves.
   Same reasoning as .content's padding-bottom above: a constant footprint is
   worth more than a precisely-sized one that arrives late. */
/* Phones only. .content sits at top:10% -- 84px on a 390x844 phone against a
   35px bar -- so the reserve is what protects a short landscape viewport, where
   10% of 390 is 39px. Desktop needs none of it: 10% of 863 is 86px against a
   31px bar, 55px of clearance, and reserving 31 more was the whole reason the
   home page stopped fitting its window. */
@media only screen and (max-width: 1100px) {
	body:not(.page-lounge) .content { margin-top: 35px; }
}

.ticker-viewport {
	display: block;
	height: 100%;
	overflow: hidden;
}

/* The run appears TWICE inside the belt and the belt travels exactly -50%, so
   at the instant the animation restarts the second copy is sitting precisely
   where the first one began and the seam is invisible. Duration is measured in
   js/listening/ticker.js so the speed is the same however long the belt is. */
.ticker-belt {
	display: flex;
	height: 100%;
	width: max-content;
	animation-name: eh-ticker;
	animation-timing-function: linear;
	animation-iteration-count: infinite;
}

.ticker-run {
	display: flex;
	align-items: center;
	height: 100%;
	white-space: nowrap;
}

@keyframes eh-ticker {
	from { transform: translateX(0); }
	to   { transform: translateX(-50%); }
}

/* A thumb on the bar stops it, so a title going past can actually be read. */
.ticker:active .ticker-belt { animation-play-state: paused; }

.ticker-item {
	display: inline-flex;
	align-items: center;
	gap: 6px;
	color: var(--muted);
}

/* Anchor points. A belt of uniform type is hard to read out of the corner of an
   eye; an image-then-text rhythm gives it somewhere to land. The empty variant
   is the same box with no picture in it, so a track Last.fm has no artwork for
   -- about one in seven -- does not break the rhythm. */
.ticker-art {
	width: 20px;
	height: 20px;
	flex: none;
	margin-right: 2px;
	border-radius: 2px;
	object-fit: cover;
	background: var(--line-soft);
}

/* The separator belongs to the item rather than to the markup, so the belt is
   a list of tracks and not a list of tracks and punctuation.

   All the spacing lives on these margins and none on the item, so the gap
   before the dot equals the gap after it. Split between item padding and this
   margin it came out 14px on one side and 28px on the other, and the dot read
   as a stray mark floating between two phrases rather than as a separator. */
.ticker-item::after {
	content: '\00B7';
	margin: 0 12px;
	color: var(--accent);
}

.ticker-lede { color: var(--accent); letter-spacing: 0.04em; }
.ticker-artist { color: var(--ink); }
.ticker-track { color: var(--muted); }
.ticker-when { color: var(--muted); opacity: .7; font-size: 11px; }

/* The track the lede is talking about, set apart from the ones merely behind
   it in the queue. */
.ticker-item.is-head .ticker-track { color: var(--ink); }

/* Desktop gets the same information in a quieter voice.

   On a phone the ticker is the only way into the Lounge -- the room is several
   screenfuls down and nothing else hints at it. On a desktop the corner already
   names the current track and offers Play, so the bar is room tone rather than
   navigation, and it should not read like a trading floor above a page whose
   whole character is stillness. Nine items are visible at this width instead of
   two, so the same speed and weight land very differently: smaller, slower, no
   artwork, one step further toward muted. */
@media only screen and (min-width: 1101px) {
	.ticker {
		height: 30px;
		/* The type size stays at 12px everywhere. Only the speed changes: nine
		   items are visible at this width against two on a phone, so the same
		   rate reads as a trading floor here and as alive there. Shrinking the
		   text as well was quieting the wrong variable. */
		--ticker-speed: 28;
	}

	/* Dropped in CSS rather than by branching the template, so renderTicker()
	   stays one code path and crossing the breakpoint needs no re-render. */
	.ticker-art { display: none; }

	/* One step down from --ink, not two. The quieting comes mostly from the
	   size, the missing artwork and the speed; taking the text further down
	   stops being recessed and starts being unreadable -- --line-strong is a
	   rule colour and sits near 1.4:1 against --ground. */
	.ticker-artist { color: var(--muted-strong); }
	.ticker-item::after { opacity: .45; }
}

/* Artwork belongs to the phone, not to a narrow window. The rule above is
   width-based, so dragging a desktop window under 1101px brought the album art
   back on a machine that is plainly not a phone. A pointer query asks the right
   question: a mouse means desktop at any width, and a finger keeps its
   artwork even in landscape. */
@media (pointer: fine) {
	.ticker-art { display: none; }
}

/* ── The photo modal (touch) ──────────────────────────────────────────────── */

.photo-modal {
	position: fixed;
	inset: 0;
	z-index: 60;
	display: flex;
	align-items: center;
	justify-content: center;
	padding: 24px;
	background: color-mix(in srgb, var(--ink) 76%, transparent);
	backdrop-filter: blur(3px);
	cursor: zoom-out;
}

.photo-modal-inner {
	margin: 0;
	max-width: 100%;
	max-height: 100%;
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.photo-modal img {
	display: block;
	max-width: 100%;
	max-height: calc(100vh - 120px);
	object-fit: contain;
	border-radius: 2px;
}

.photo-modal figcaption {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: 16px;
	color: var(--ground);
	font-size: 13px;
}

.photo-modal figcaption a {
	color: var(--ground);
	border-bottom: 1px solid color-mix(in srgb, var(--ground) 50%, transparent);
	cursor: pointer;
}

/* Stops the page scrolling behind the photograph. */
body.has-modal { overflow: hidden; }

@media (prefers-reduced-motion: no-preference) {
	.photo-modal { animation: eh-fade .16s ease-out; }
}

@keyframes eh-fade { from { opacity: 0 } to { opacity: 1 } }
