/* Touch and small screens. Two separate axes: pointer:coarse is about the
   input (44px targets), max-width is about the room. Written in that order. */

/* === Mobile boards: Feed and Playlist ===
   Two DOM scrollers covering the canvas, toggled by data-feed-lens on <html>.
   Keyed off attribute not width — entry is a manual choice, tablets get them too. */
#mobile-feed, #mobile-playlist {
  --feed-inset-top: calc(max(12px, env(safe-area-inset-top)) + var(--chrome-button-h) + 20px);
  --feed-inset-bottom: calc(calc(max(12px, env(safe-area-inset-bottom)) + var(--dock)) + var(--chrome-button-h) + 12px);
  display: none;
  position: fixed;
  inset: 0;
  z-index: 2;
  box-sizing: border-box;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior-y: contain;
  /* The scrollbar's room kept on both sides, so the column centres on the
     window - the centre line the now-playing bar under it keeps (chrome.css). */
  scrollbar-gutter: stable both-edges;
  background: color-mix(in srgb, var(--ink) 6%, var(--paper));
  contain: layout paint;
  -webkit-overflow-scrolling: touch;
}
#mobile-feed { --feed-w: 940px; }
#mobile-playlist {
  --feed-w: 520px;
  --pl-gutter: clamp(14px, 4.5vw, 22px);
  /* Clears the now-playing bar at the foot */
  --feed-inset-bottom: calc(calc(max(16px, env(safe-area-inset-bottom)) + var(--dock)) + 78px);
}

:root[data-board-mode="mobile"]:not([data-feed-lens="playlist"]) #mobile-feed { display: block; }
:root[data-board-mode="mobile"][data-feed-lens="playlist"] #mobile-playlist { display: block; }
/* visibility not display — viewport.ts caches the box size */
:root[data-board-mode="mobile"] #viewport { visibility: hidden; }
/* Pen only shows on Feed (editable masthead), not Playlist (album hero) */
:root[data-board-mode="mobile"][data-feed-lens="playlist"] #mobile-header-edit-btn { display: none; }
/* Playlist has no selection or undo — editing chrome hidden */
:root[data-board-mode="mobile"][data-feed-lens="playlist"] .drawer-row { display: none; }
/* The Playlist's column moves right as the sidebar comes in, and centres in
   the room the panel leaves, as the toolbar does (chrome.css). Padding and not
   `left`, so the column moves and the background stays under the panel.
   #mobile-playlist sits before #sidebar in index.html, so a sibling rule
   cannot reach it. Above 600 px only: narrower, the panel covers the screen. */
@media not all and (max-width: 600px) {
  #mobile-playlist { transition: padding-left var(--dur-base) var(--ease-back); }
  :root:has(#sidebar.is-open) #mobile-playlist {
    padding-left: calc(max(var(--panel-inset), env(safe-area-inset-left)) + min(var(--sidebar-w), 100vw - 28px));
  }
}

.feed-sheet {
  box-sizing: border-box;
  position: relative;
  width: min(100% - 24px, var(--feed-w));
  margin-inline: auto;
  margin-block: var(--feed-inset-top) var(--feed-inset-bottom);
  min-height: calc(100dvh - var(--feed-inset-top) - var(--feed-inset-bottom));
  /* No side padding — masthead is full-bleed, tiles inset themselves */
  padding: 0 0 20px;
  overflow: hidden;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--accent) 6%, #fff);
  box-shadow: var(--shadow-1);
}

/* On a phone the Feed's sheet is the width of the screen, with square corners,
   and the cards stand one card gap (GAP in ui/feed.ts) from its edges. Its top
   and foot stay under the buttons and over the bar
   (research/old/done-2026-09-15.md, R19). */
@media (max-width: 600px) {
  #mobile-feed .feed-sheet { width: 100%; border-radius: 0; }
  #mobile-feed .feed-grid { width: calc(100% - 20px); }
}

/* ::before so it multiplies against the sheet fill, not the photographs */
.feed-sheet::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

/* Masthead — the board name, styled by styleFeedMasthead() in feed/playlist.
   Width capped so the 3:2 band does not overgrow on desktop sheets. */
.feed-masthead {
  width: 100%;
  max-width: 440px;
  margin-inline: auto;
  aspect-ratio: 3 / 2;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px 0;
  overflow: hidden;
}
.feed-title {
  margin: 0;
  min-width: 0;
  max-width: 100%;
  font-family: var(--font-display);
  font-size: calc(clamp(20px,
    calc(var(--mobile-board-width, 0px) * var(--mobile-title-scale, 0.13)),
    calc(var(--mobile-board-width, 0px) * var(--mobile-title-cap, 0.1875))
  ) * var(--mobile-title-fit, 1));
  transform:
    translateY(calc(var(--mobile-header-height, 0px) * var(--mobile-title-offset, 0) / 100))
    scaleY(var(--mobile-title-stretch, 1));
  transform-origin: 50% 52%;
  line-height: normal;
  padding-block: 0.2em 0.45em;
  text-align: center;
  text-wrap: balance;
  overflow-wrap: anywhere;
  color: var(--ink);
  display: -webkit-box;
  -webkit-line-clamp: 4;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.feed-title[data-nowrap] { white-space: pre; flex: none; max-width: none; }
.feed-title.is-default { opacity: 0.45; }
/* A press on the name opens the masthead editor (ui/feed.ts). */
.feed-title[role="button"] { cursor: pointer; }

/* Masonry grid — tiles positioned absolutely by ui/feed.ts */
.feed-grid {
  position: relative;
  width: calc(100% - 24px);
  margin: 0 auto;
}

/* content-visibility: auto skips paint for offscreen tiles */
.feed-tile {
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  border-radius: var(--leaf);
  background: var(--paper-3);
  box-shadow: var(--shadow-1);
  content-visibility: auto;
}
/* Wherever card shadows are on, a tile stands off the sheet on a soft drop
   shadow (R8). Harsh stays flat, as its cards on the canvas do. Keyed to the
   shadow flag rather than to the Full stop, which left Balanced - shadows on -
   with flat tiles and made the Card shadows switch do nothing here. The third
   :not() is Light before ui/quality.ts has written the flags (tokens.css). */
:root:not([data-q-shadows="off"], [data-whimsy="2"], [data-quality="light"]:not([data-q-shadows])) .feed-tile {
  box-shadow: 0 1px 2px color-mix(in srgb, var(--ink) 12%, transparent),
              0 4px 12px -2px color-mix(in srgb, var(--ink) 24%, transparent);
}
/* A fence's name across the wall, over its cards (feedRuns() in ui/feed.ts).
   The caption rule of the fence on the canvas (fences.css), laid flat. */
.feed-band {
  position: absolute;
  top: 0;
  left: 0;
  display: flex;
  align-items: flex-end;
  box-sizing: border-box;
  padding: 0 2px 6px;
  border-bottom: 1px solid var(--rule);
}
.feed-band-name {
  font-family: var(--font-body);
  font-size: var(--t-small);
  font-weight: 600;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.feed-tile > img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Fill vs fit follows data-fit, the same attribute the canvas card keys
   object-fit off (items.css). Written by writeTileFit() in ui/feed.ts from the
   board-wide Fill/Fit option and any per-item override, so a photo set to Fit on
   the board fits here too - letterboxed against the tile's own paper. */
.feed-tile[data-fit="contain"] > img,
.feed-tile[data-fit="contain"] video { object-fit: contain; }

/* Loading shimmer: shown while a tile's picture resolves - ui/feed.ts adds
   .is-loading and drops it on the first paint. GPU-only, no image decode, and it
   sits under the <img>, so it vanishes the instant real pixels land. Offscreen
   tiles skip it for free (content-visibility above). The tile's own --paper-3
   shows through the transparent band. */
.feed-tile.is-loading::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg, transparent 30%,
    color-mix(in srgb, var(--ink) 7%, transparent) 50%, transparent 70%);
  background-size: 220% 100%;
  animation: feed-shimmer 1.3s ease-in-out infinite;
}
@keyframes feed-shimmer { from { background-position: 120% 0; } to { background-position: -120% 0; } }

/* Video: a poster with a play badge, swapped for a live <video> on tap. */
.feed-tile[data-kind="video"] { cursor: pointer; }
.feed-play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: #fff;
  background: color-mix(in srgb, #000 18%, transparent);
}
.feed-play svg { width: 34px; height: 34px; filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.5)); }
/* Over a playing clip the badge goes; a tap pauses and it comes back. */
.feed-tile.is-playing .feed-play { display: none; }
/* The tile Find picked (revealInFeed() in ui/feed.ts): a ring that holds, then
   fades, since the Feed has no selection mark to show the hit by. An outline, so the tile's own shadow at each quality stop is left alone. */
.feed-tile.is-found {
  outline: 3px solid var(--accent-text);
  outline-offset: 2px;
  animation: feed-found 1.6s ease-out both;
}
@keyframes feed-found {
  0%, 60% { outline-color: var(--accent-text); }
  100% { outline-color: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .feed-tile.is-found { animation: none; }
}
/* A selected tile (ui/feed.ts paints it off the board's selection): the
   canvas's selection colour, as a ring outside the tile so the picture stays
   whole. An outline, as the Find ring above is. */
.feed-tile.is-selected {
  outline: 2.5px solid var(--select);
  outline-offset: 2px;
}
/* The multi-select mode. On the canvas it is a frame round the window, but
   here the window is mostly tiles, so each tile a tap now picks wears an empty
   circle, and a picked one a filled circle with a tick. Top left, clear of the
   play buttons in the middle of the audio and video tiles. Square at Harsh, as
   the sidebar's checkboxes are. */
:root.is-picking .feed-tile:not([data-kind="hint"])::after {
  content: '';
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 3;
  display: grid;
  place-items: center;
  width: 22px;
  height: 22px;
  box-sizing: border-box;
  border: 2px solid #fff;
  border-radius: var(--radius-pill);
  background: rgba(0, 0, 0, 0.22);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  color: #fff;
  font: 700 13px/1 var(--font-body);
  pointer-events: none;
}
:root.is-picking .feed-tile.is-selected::after {
  content: '\2713';
  background: var(--select);
}
.feed-tile video { width: 100%; height: 100%; object-fit: cover; background: #000; display: block; }

/* A card drawn by the canvas's renderer at its board size, scaled to the tile
   by --feed-s (fillCard() in ui/feed.ts). The card brings its own paper. */
.feed-tile[data-kind="card"] { background: none; }
.feed-card {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  transform: scale(var(--feed-s, 1));
}
.feed-card > .card { width: 100%; height: 100%; box-sizing: border-box; }

/* Audio: a cover (or a note glyph) with the track name over a gradient foot. A tap
   goes to the Playlist, audio's home. */
.feed-tile[data-kind="audio"] {
  cursor: pointer;
  background: color-mix(in srgb, var(--accent) 10%, var(--paper-3));
}
.feed-tile-art { position: absolute; inset: 0; }
.feed-tile-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
.feed-tile-art.is-placeholder {
  display: grid;
  place-items: center;
  color: color-mix(in srgb, var(--accent-text) 70%, var(--ink-3));
}
.feed-tile-art.is-placeholder svg { width: 34%; height: 34%; }
/* The still waveform on a coverless audio tile: a row of bars at fixed heights,
   the same motif the canvas card and the playing playlist row carry. */
.feed-bars {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 58%;
  height: 40%;
}
.feed-bars i {
  flex: 1;
  min-width: 3px;
  border-radius: var(--radius-pill);
  background: color-mix(in srgb, var(--accent) 58%, var(--ink-3));
}
/* The audio tile's play button: a play glyph on a paper chip in the middle of
   the tile, where the video tile has its own, saying a tap plays it. The glyph
   takes no nudge: PLAY_ICON's path already sits a little right of its box,
   which is what centres a triangle to the eye. */
.feed-tile-badge {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 44px; height: 44px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  color: var(--accent-text);
  box-shadow: var(--shadow-1);
}
.feed-tile-badge svg { width: 22px; height: 22px; }
.feed-tile-cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 16px 10px 9px;
  font-family: var(--font-body);
  color: #fff;
  background: linear-gradient(transparent, rgba(0, 0, 0, 0.62));
}
.feed-cap-title {
  font-size: var(--t-small);
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.feed-cap-sub {
  margin-top: 1px;
  font-size: var(--t-tiny);
  opacity: 0.82;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.feed-note {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 12px;
  font-family: var(--font-body);
  font-size: var(--t-small);
  line-height: 1.4;
  color: var(--ink);
  background: var(--paper-card);
  white-space: pre-wrap;
  word-break: break-word;
  display: -webkit-box;
  -webkit-line-clamp: 9;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The notes stuck on a card ride its tile (paintRiders() in ui/feed.ts). The
   button is a small sticky in the top-right corner with the count on it, clear
   of the pick circle top left and the play badge in the middle. */
.feed-notes-btn {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 4;
  min-width: 26px;
  height: 26px;
  padding: 0 7px;
  box-sizing: border-box;
  border: 0;
  border-radius: var(--radius-xs);
  background: var(--note-1);
  color: var(--ink);
  font: 700 var(--t-small)/1 var(--font-body);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  cursor: pointer;
}
.feed-notes-btn[aria-expanded="true"],
.feed-notes-btn:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 1px;
}
/* The notes laid open over the tile, on a scrim, under the button. A long
   stack scrolls inside the tile rather than growing it. */
.feed-notes {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 42px 8px 8px;
  box-sizing: border-box;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: rgba(0, 0, 0, 0.4);
}
.feed-rider {
  flex: none;
  padding: 10px 12px;
  border-radius: var(--radius-xs);
  background: var(--note-1);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--t-small);
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
  cursor: pointer;
}

.feed-link {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 12px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 4px;
  background: var(--paper-card);
}
/* The mark and the name on one row - the site's own logo (renderers.ts
   linkGlyph) beside what the link is called, so a Feed link reads like its
   canvas card. The name still ellipses; the mark keeps its size. */
.feed-link-title {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.feed-link-title svg { width: 18px; height: 18px; flex: none; }
.feed-link-name {
  font-family: var(--font-body);
  font-weight: 600;
  color: var(--ink);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.feed-link-host {
  font-size: var(--t-small);
  color: var(--accent-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* A link that is a player: the offer, until it is tapped. The tile is already
   the player's height (sizeEmbeds), so this is only what fills it before the
   press - the site's name at the top, the offer chip (.embed-go, cards.css)
   centred by its own margin in the space below. On a tap fillLink's mountEmbed
   replaces the lot with the frame. */
.feed-embed {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  background: var(--paper-card);
  cursor: pointer;
}
/* The offer chip is a toolbar-sized affordance on the canvas; on a touch wall
   it is the whole point of the tile and the thing a thumb aims at, so it is
   larger here - bigger type, more padding, a bigger glyph. Feed-scoped, so the
   canvas chip keeps its restrained size. */
.feed-embed .embed-go {
  padding: 11px 18px 11px 15px;
  gap: 8px;
  font-size: var(--t-body);
}
.feed-embed .embed-go svg { width: 19px; height: 19px; }
/* The mounted frame fills the tile, which clips it to the tile's own radius -
   so no second corner to disagree with, unlike the canvas card. */
.feed-embed-frame {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

.feed-swatch { position: absolute; inset: 0; }
.feed-swatch-label {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 4px 8px;
  text-align: center;
  font-size: var(--t-tiny);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
  background: color-mix(in srgb, var(--paper) 78%, transparent);
}

/* No scroll — tile is a glance, full file is one tap away in the viewer */
.feed-text {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  overflow: hidden;
  background: var(--paper-card);
}
.feed-text-name {
  flex: none;
  max-width: 100%;
  color: var(--ink);
  font-size: var(--t-small);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.feed-text-body,
.feed-text-md {
  flex: 1 1 auto;
  min-height: 0;
  margin: 0;
  overflow: hidden;
  color: var(--ink-2);
  font-family: var(--font-mono);
  font-size: var(--t-tiny);
  line-height: 1.45;
  white-space: pre-wrap;
  word-break: break-word;
  /* Mask not overlay — tile and wall have different fills */
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2.4em), transparent);
  mask-image: linear-gradient(to bottom, #000 calc(100% - 2.4em), transparent);
}

/* Markdown body — tighter block spacing than the viewer, inherits clip/fade */
.feed-text-md {
  font-family: inherit;
  white-space: normal;
  line-height: 1.4;
}
.feed-text-md > :first-child { margin-top: 0; }
.feed-text-md h1, .feed-text-md h2, .feed-text-md h3,
.feed-text-md h4, .feed-text-md h5, .feed-text-md h6 {
  margin: 0.6em 0 0.25em;
  color: var(--ink);
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.2;
}
.feed-text-md h1 { font-size: 1.45em; }
.feed-text-md h2 { font-size: 1.2em; }
.feed-text-md h3, .feed-text-md h4,
.feed-text-md h5, .feed-text-md h6 { font-size: 1.05em; }
.feed-text-md p { margin: 0 0 0.5em; }
.feed-text-md ul, .feed-text-md ol { margin: 0 0 0.5em; padding-left: 1.2em; }
.feed-text-md li.md-task { list-style: none; margin-left: -1.1em; }
.feed-text-md blockquote {
  margin: 0 0 0.5em;
  padding-left: 0.6em;
  border-left: 2px solid var(--rule);
}
.feed-text-md hr { margin: 0.6em 0; border: 0; border-top: var(--hairline) solid var(--rule-2); }
.feed-text-md code { font-family: var(--font-mono); font-size: 0.9em; }
.feed-text-md pre {
  margin: 0 0 0.5em;
  overflow: hidden;
}
/* Links disabled — tile is one tap target, links are live in the viewer */
.feed-text-md a { color: inherit; pointer-events: none; text-decoration: underline; }
.feed-text-md table { display: none; }

/* Ghost card brings its own fill and shadow — no doubling */
.feed-tile[data-kind="hint"] {
  background: none;
  box-shadow: none;
  overflow: visible;
  /* measureHints() reads scrollHeight — content-visibility:auto would report containment box */
  content-visibility: visible;
}
.feed-hint { width: 100%; height: 100%; }
/* height:auto so measureHints() can read scrollHeight; min-height fills once packed */
.feed-hint > .card {
  width: 100%;
  height: auto;
  min-height: 100%;
  box-sizing: border-box;
}

.feed-file {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 12px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  background: var(--paper-card);
}
.feed-file-name {
  max-width: 100%;
  color: var(--ink);
  font-size: var(--t-small);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.feed-file-kind {
  max-width: 100%;
  color: var(--ink-3);
  font-size: var(--t-tiny);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
}

/* Class not [hidden] because display:flex would beat it */
.feed-empty { display: none; }
.feed-empty.is-shown {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-height: 38vh;
  padding: 20px;
  text-align: center;
  font-family: var(--font-body);
}
.feed-empty-title { margin: 0; color: var(--ink-2); font-size: var(--t-title); }
.feed-empty-body { margin: 0; max-width: 34ch; color: var(--ink-3); font-size: var(--t-small); line-height: 1.5; }
/* The empty Playlist lens's way back: the Shuffle button's quiet outline,
   since it takes the reader somewhere rather than starting anything. */
.feed-empty-back {
  margin-top: 8px;
  padding: 10px 20px;
  border: var(--hairline) solid var(--rule);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--ink-2);
  font-family: var(--font-body);
  font-size: var(--t-body);
  font-weight: 600;
  cursor: pointer;
}
@media (hover: hover) {
  .feed-empty-back:hover { background: var(--paper-2); color: var(--accent-deep); }
}
.feed-empty-back:active { background: var(--paper-3); }

/* Playlist hero — sleeve, record disc (CSS gradients), title, Play/Shuffle.
   The disc spins while playing; drawn from the palette accent. */
.pl-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  /* The sleeve and its two buttons, not a text to copy - see .pl-list. */
  -webkit-user-select: none;
  user-select: none;
  padding: 30px var(--pl-gutter, 20px) 22px;
  text-align: center;
}
.pl-hero.is-empty { display: none; }
.pl-sleeve {
  position: relative;
  width: clamp(150px, 44vw, 206px);
  aspect-ratio: 1;
}
/* Record disc is Softish-only ornament — off by default */
:root[data-whimsy="0"] .pl-sleeve { transform: translateX(-9%); }
.pl-disc {
  display: none;
  position: absolute;
  top: 7%;
  bottom: 7%;
  right: -18%;
  aspect-ratio: 1;
  border-radius: 50%;
  /* closest-side so % stops map to the disc edge, not the diagonal */
  background:
    radial-gradient(circle closest-side at 50% 50%,
      var(--paper-card) 0 5%, transparent 5.5%),
    radial-gradient(circle closest-side at 50% 50%,
      var(--accent) 5.5% 33%, transparent 33.5%),
    repeating-radial-gradient(circle closest-side at 50% 50%,
      color-mix(in srgb, var(--ink) 86%, transparent) 0 1.5px,
      color-mix(in srgb, var(--ink) 70%, transparent) 1.5px 3.5px);
  box-shadow: var(--shadow-1);
}
:root[data-whimsy="0"] .pl-disc { display: block; }
.pl-hero.is-spinning .pl-disc { animation: pl-spin 5.5s linear infinite; }
@keyframes pl-spin { to { rotate: 1turn; } }
@media (prefers-reduced-motion: reduce) {
  .pl-hero.is-spinning .pl-disc { animation: none; }
}
.pl-cover {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: var(--radius);
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 12%, var(--paper-3));
  box-shadow: var(--shadow-2);
}
/* Overlay not inset shadow — inset paints under children, cover art would hide it */
.pl-cover::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  box-shadow: inset 0 0 0 var(--hairline) color-mix(in srgb, var(--ink) 12%, transparent);
  pointer-events: none;
}
.pl-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pl-cover.is-mosaic { display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.pl-cover.is-placeholder {
  display: grid;
  place-items: center;
  color: color-mix(in srgb, var(--accent-text) 65%, var(--ink-3));
}
.pl-cover.is-placeholder svg { width: 44%; height: 44%; }
.pl-hero-text { display: flex; flex-direction: column; align-items: center; gap: 8px; width: 100%; min-width: 0; }
.pl-hero-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(24px, 6.8vw, 34px);
  line-height: 1.08;
  letter-spacing: -0.01em;
  color: var(--ink);
  max-width: 20ch;
  text-wrap: balance;
}
.pl-hero-title.is-default { color: var(--ink-2); }
.pl-hero-meta {
  font-family: var(--font-body);
  font-size: var(--t-tiny);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-3);
}
.pl-hero-actions { display: flex; gap: 8px; justify-content: center; align-items: center; flex-wrap: wrap; margin-top: 10px; }
/* Big, and they can be: they are the only controls on the lens now that the
   transport is the bar at the foot of the window. Two weights rather than two of
   the same button - Play is what this header is for and carries the fill,
   Shuffle is the other way in and is a hairline. They were a matched pair of
   132px pills, which is what made the header read as a dialog asking a question
   rather than a record with a way to start it. */
.pl-play, .pl-shuffle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 15px 26px;
  border-radius: var(--radius-pill);
  font-family: var(--font-body);
  font-size: calc(var(--t-body) + 1px);
  font-weight: 600;
  cursor: pointer;
  border: var(--hairline) solid transparent;
  transition: transform var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
/* --accent-fg and --accent-deep rather than #fff and a black mix: these two are
   the loudest controls on the phone and they were the pair that did not move
   with the palette, so a teal or sage board still printed cold white on its
   primary button and darkened it towards grey on hover. */
.pl-play {
  min-width: 172px;
  background: var(--accent);
  color: var(--accent-fg);
  border-color: var(--accent-text);
  box-shadow: var(--shadow-1);
}
.pl-shuffle { background: transparent; color: var(--ink-2); border-color: var(--rule); }
/* Guarded and stepped back to --paper-2. This is the phone's playlist: every
   hover in it was answering a tap and staying lit, and the wash they were using
   is the one tokens.css names for a press. */
@media (hover: hover) {
  .pl-play:hover { background: var(--accent-deep); }
  .pl-shuffle:hover { background: var(--paper-2); color: var(--accent-deep); }
}
.pl-shuffle:active { background: var(--paper-3); }
.pl-shuffle.is-on {
  background: color-mix(in srgb, var(--accent) 14%, var(--paper-card));
  border-color: var(--accent-text);
  color: var(--accent-text);
}
.pl-play:active, .pl-shuffle:active { transform: translateY(1px); }
.pl-play .pl-ico, .pl-shuffle .pl-ico { display: inline-flex; }
.pl-play svg, .pl-shuffle svg { width: 18px; height: 18px; }

/* The Playlist list, in normal flow. In the Mobile lens it sits on the board
   sheet; in the Desktop window it fills the window body. */
.pl-list {
  display: flex;
  flex-direction: column;
  margin: 0 var(--pl-gutter, 12px) 10px;
  /* A list of controls, not a text: a drag or a double click selected the
     titles and times in orange. The one field typed into takes it back. */
  -webkit-user-select: none;
  user-select: none;
  /* The numbering behind the track-number tiles. On the list rather than on the
     rows themselves, so a reorder renumbers by itself - the DOM order *is* the
     order, and nothing has to walk the list to say so. */
  counter-reset: pl-track;
}
.player-window-body .pl-list { width: 100%; margin: 0; }

/* A track row, Apple-Music/Spotify shape: cover, title over artist, duration, and
   - only in a reorderable list - a grip to drag it by. The whole row is the play
   control; the shared queue plays and the now-playing bar drives it. The current
   row lights in the accent and its cover carries a small equalizer. */
.pl-row {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  box-sizing: border-box;
  padding: 8px 8px 8px 10px;
  /* The player window's corner less the 12px its list keeps from it, so the
     last row's corner runs parallel to the window's. */
  border-radius: max(var(--radius-xs), calc(var(--radius) - 12px));
  cursor: pointer;
  counter-increment: pl-track;
  transition: opacity var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease);
}
/* No rule between rows. A top border on a rounded row bends into its corners,
   so on the soft tier every hairline drew as a faint arc over the row below.
   The rows are told apart by their covers, number tiles and even spacing. */
@media (hover: hover) {
  .pl-row:hover { background: var(--paper-2); }
}
.pl-row:active { background: var(--paper-3); }
.pl-row.is-playing { background: color-mix(in srgb, var(--accent) 10%, var(--paper-card)); }
/* A selected track (ui/playlist.ts), in the canvas's selection colour. Inset,
   because the sheet clips at its edges and a ring outside would lose its ends. */
.pl-row.is-selected { box-shadow: inset 0 0 0 2px var(--select); }
/* The multi-select mode: each row a tap now picks wears the Feed tile's empty
   circle, and a picked one the filled circle with a tick. On the row's corner
   and not the cover's, where it would sit on the track's number. */
:root.is-picking .pl-row::after {
  content: '';
  position: absolute;
  top: 2px;
  left: 2px;
  z-index: 1;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  box-sizing: border-box;
  border: 2px solid #fff;
  border-radius: var(--radius-pill);
  background: rgba(0, 0, 0, 0.22);
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
  color: #fff;
  font: 700 11px/1 var(--font-body);
}
:root.is-picking .pl-row.is-selected::after {
  content: '\2713';
  background: var(--select);
}
/* Reordering (ui/playlist.ts). The held row lifts off the sheet and follows
   the pointer on `translate`, which is set from script and so never eased; the
   rows it passes slide aside on theirs. The slide is on only while sorting: on
   the drop the offsets are cleared in the same frame the rows move in the DOM,
   and an eased clear would slide each one back out of its new place. */
.pl-list.is-sorting .pl-row:not(.is-dragging) {
  transition: translate var(--dur-base) var(--ease), opacity var(--dur-base) var(--ease);
}
.pl-list.is-sorting .pl-row { cursor: grabbing; }
.pl-row.is-dragging {
  background: var(--paper-card);
  box-shadow: var(--shadow-2);
  z-index: 2;
  scale: 1.03;
  /* A quarter of a card's tilt: a row is three cards wide, and the full angle
     swings its far end by more than its own height. Zero off the playful tier. */
  rotate: calc(var(--tilt-drag) / 4);
  transition: scale var(--dur-fast) var(--ease), rotate var(--dur-fast) var(--ease),
    box-shadow var(--dur-fast) var(--ease);
}
/* The drop: script eases it into its slot; this keeps it above the rows it
   lands among until it does, while its shadow fades on .pl-row's transition. */
.pl-row.is-settling {
  background: var(--paper-card);
  z-index: 2;
}
/* Hidden from the playlist: greyed at the foot, out of the numbering, and set
   a little apart from the tracks that play. */
.pl-row.is-hidden { opacity: 0.45; counter-increment: none; }
.pl-row:not(.is-hidden) + .pl-row.is-hidden { margin-top: 14px; }
.pl-row.is-hidden .pl-art img { filter: grayscale(1); }
.pl-row.is-hidden .pl-art.is-placeholder::before { content: "\2013"; }
.pl-row.is-hidden .pl-grip { visibility: hidden; }
@media (hover: hover) {
  .pl-row.is-hidden:hover { opacity: 0.7; }
}
.pl-art {
  position: relative;
  flex: none;
  width: 44px;
  height: 44px;
  /* 8px inside the row's corner, so the row's corner less that, near enough. */
  border-radius: var(--radius-xs);
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 12%, var(--paper-3));
}
.pl-art img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No embedded art: the track's number, not a fifth identical note glyph. Most
   loose MP3s carry no picture, so the glyph was the common case and it printed
   the same tile down the whole list - a column that said nothing nine times.
   The number is drawn from the list's counter, so a drag renumbers it. */
.pl-art.is-placeholder {
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--accent) 9%, var(--paper-2));
  color: color-mix(in srgb, var(--ink-2) 75%, var(--accent-text));
  font-family: var(--font-mono);
  font-size: var(--t-body);
  font-variant-numeric: tabular-nums;
}
.pl-art.is-placeholder::before { content: counter(pl-track); }
.pl-row.is-playing .pl-art.is-placeholder {
  color: var(--accent-text);
  background: color-mix(in srgb, var(--accent) 14%, var(--paper-2));
}
.pl-main { flex: 1 1 auto; min-width: 0; }
.pl-title {
  font-family: var(--font-body);
  font-size: var(--t-body);
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pl-row.is-playing .pl-title { color: var(--accent-text); font-weight: 600; }
/* Rename, in place (ui/playlist.ts renameRow). */
.pl-row.is-renaming { cursor: default; }
.pl-row.is-renaming .pl-title {
  -webkit-user-select: text;
  user-select: text;
  cursor: text;
  text-overflow: clip;
  outline: var(--hairline) solid var(--accent-text);
  outline-offset: 2px;
  border-radius: 2px;
}
.pl-artist {
  font-size: var(--t-small);
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Right-ranged in a column of its own width, so nine durations make one edge
   rather than nine. */
.pl-time {
  flex: none;
  min-width: 4ch;
  text-align: right;
  font-size: var(--t-small);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.pl-row.is-playing .pl-time { color: color-mix(in srgb, var(--accent-text) 70%, var(--ink-3)); }
.pl-grip {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 0;
  border-radius: var(--leaf);
  background: none;
  color: var(--ink-3);
  cursor: grab;
  touch-action: none;
}
@media (hover: hover) {
  .pl-grip:hover { background: var(--paper-2); color: var(--ink-2); }
}
.pl-grip:active { background: var(--paper-3); }
.pl-row.is-dragging .pl-grip { cursor: grabbing; }
.pl-grip svg { width: 16px; height: 16px; }
/* Where there is a pointer the handle waits to be asked for. Nine of them
   standing at the right margin read as a form, and the row is the play control -
   the grip is the exception to it. Where there is no hover to ask with it stays
   put, because this is the one gesture on the lens that has no other way in.

   `hover` alone and not `and (pointer: fine)`: the question is whether the
   surface can show a control on approach, which is what hover answers. A
   touchscreen laptop with a mouse reports a coarse primary pointer and would
   have been read as a phone. */
@media (hover: hover) {
  .pl-grip { opacity: 0; transition: opacity var(--dur-fast) var(--ease); }
  .pl-row:hover .pl-grip,
  .pl-row.is-dragging .pl-grip,
  .pl-grip:focus-visible { opacity: 1; }
}

/* The equalizer over the cover, shown only on the playing row. Four bars of a
   constant width; the height is what dances. */
.pl-eq {
  position: absolute;
  inset: 0;
  display: none;
  align-items: flex-end;
  justify-content: center;
  gap: 2px;
  padding: 9px 0;
  background: color-mix(in srgb, #000 34%, transparent);
}
.pl-row.is-playing .pl-eq { display: flex; }
.pl-eq i {
  width: 3px;
  height: 100%;
  border-radius: var(--radius-pill);
  background: #fff;
  transform-origin: bottom;
  animation: pl-eq 0.9s ease-in-out infinite;
}
.pl-eq i:nth-child(1) { animation-delay: -0.20s; }
.pl-eq i:nth-child(2) { animation-delay: -0.55s; }
.pl-eq i:nth-child(3) { animation-delay: -0.10s; }
.pl-eq i:nth-child(4) { animation-delay: -0.70s; }
@keyframes pl-eq {
  0%, 100% { transform: scaleY(0.25); }
  50% { transform: scaleY(1); }
}

/* The Desktop Playlist: a panel anchored to the bottom-right above the
   now-playing bar. It stands over the board, sized to its content up to a cap,
   and cannot be moved or resized. It flies in from the right edge the way
   #sidebar does from the left, and shares that edge with the sticker pad and
   the header edit bar through ui/panel-stack.ts - opening one of the three
   puts the others away and closing it brings the last one back. */
.player-window {
  position: fixed;
  z-index: var(--z-overlay);
  right: max(16px, env(safe-area-inset-right));
  bottom: calc(max(14px, env(safe-area-inset-bottom)) + var(--dock));
  width: min(340px, calc(100vw - 32px));
  max-height: min(72vh, 620px);
  display: flex;
  flex-direction: column;
  background: var(--paper-card);
  border: var(--hairline) solid var(--rule-2);
  /* --leaf, the sidebar's corner: the same floating sheet. */
  border-radius: var(--leaf);
  box-shadow: var(--shadow-2);
  overflow: hidden;
  /* The sticker pad's entrance, which is #sidebar's mirrored: out past the
     right edge, in on --ease-back. The 22px clears the shadow. */
  transform: translateX(calc(100% + 22px));
  opacity: 0;
  visibility: hidden;
  transition: transform var(--dur-base) var(--ease-back),
              opacity var(--dur-fast) var(--ease),
              visibility 0s linear var(--dur-base);
}
.player-window.is-open {
  transform: none;
  opacity: 1;
  visibility: visible;
  transition-delay: 0s;
}
.player-window-head {
  flex: none;
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 12px 18px;
  border-bottom: var(--hairline) solid var(--rule-2);
  -webkit-user-select: none;
  user-select: none;
}
.player-window-title {
  font-family: var(--font-display);
  font-size: var(--t-title);
  color: var(--ink);
}
.player-window-spacer { flex: 1 1 auto; }
.player-window-toggle,
.player-window-close {
  flex: none;
  display: grid;
  place-items: center;
  width: var(--icon-button);
  height: var(--icon-button);
  padding: 0;
  border: 0;
  border-radius: var(--leaf);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* Guarded, and on --paper-2 rather than --paper-3. This is the phone's player
   and it had both faults at once: the hover step was the press step, and it was
   unguarded, so a tap left the button lit at the pressed colour until you
   touched something else. */
@media (hover: hover) {
  .player-window-toggle:hover,
  .player-window-close:hover { background: var(--paper-2); color: var(--accent-deep); }
}
.player-window-toggle:active,
.player-window-close:active { background: var(--paper-3); }
.player-window-toggle:focus-visible,
.player-window-close:focus-visible { background: var(--paper-2); color: var(--accent-deep); }
.player-window-toggle svg,
.player-window-close svg { width: var(--icon-glyph); height: var(--icon-glyph); }
/* The region under the title bar. It holds the body and takes the leftover
   height; the body scrolls inside it. */
.player-window-content {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.player-window-body {
  flex: 1 1 auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 6px 8px 12px;
}
.player-window-body .feed-empty.is-shown { min-height: 30vh; }

/* The window's player: a real transport, not a play-all button. The now-playing
   track (cover, title, artist) over a seek line and the five controls, driving the
   same shared queue the list below does. */
.pw-player {
  flex: none;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 14px 18px 12px;
  border-bottom: var(--hairline) solid var(--rule-2);
}
/* display:flex above beats the [hidden] default, so it has to be said. ui/playlist.ts
   sets it on a board with no audio - see fill(). */
.pw-player[hidden] { display: none; }
.pw-top { display: flex; align-items: center; gap: 12px; min-width: 0; }
.pw-cover {
  flex: none;
  width: 64px;
  height: 64px;
  border-radius: var(--leaf);
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 12%, var(--paper-3));
  box-shadow: var(--shadow-1);
}
.pw-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pw-cover.is-placeholder { display: grid; place-items: center; color: color-mix(in srgb, var(--accent-text) 65%, var(--ink-3)); }
.pw-cover.is-placeholder svg { width: 46%; height: 46%; }
.pw-info { flex: 1 1 auto; min-width: 0; }
.pw-title {
  font-family: var(--font-body);
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pw-artist {
  font-size: var(--t-small);
  color: var(--ink-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pw-seek { display: flex; align-items: center; gap: 8px; }
.pw-time {
  flex: none;
  min-width: 4ch;
  font-size: var(--t-tiny);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.pw-time:last-child { text-align: right; }
/* The window's scrubber, drawn as the same three-svg shape the now-playing bar
   and the video card use so all three wave together at the soft end of the
   whimsy axis - see the note in util.ts. It was a ::before rail and a div scaled
   on X, which cannot carry a wave: scaling one horizontally changes its
   frequency as the track plays. */
.pw-line { position: relative; flex: 1 1 auto; height: 14px; cursor: pointer; touch-action: none; }
.pw-line-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
}
/* The unplayed line is clipped to the part *past* the playhead, so the grey does
   not sit under the played wave and the two meet at --pw-progress. The base svg
   is the direct child; the fill's are inside .pw-line-fill. */
.pw-line > .pw-line-svg { clip-path: inset(0 0 0 calc(var(--pw-progress, 0) * 100%)); }
.pw-line-base { stroke: color-mix(in srgb, var(--ink) 20%, transparent); }
.pw-line-fill {
  position: absolute;
  inset: 0;
  clip-path: inset(0 calc((1 - var(--pw-progress, 0)) * 100%) 0 0);
}
.pw-line-fill-line,
.pw-line-fill-wave { stroke: var(--accent-text); }
/* view-box, so the scroll translates in the wave's own user units - which
   sizeSeekWave has made one-to-one with pixels - rather than in the path's
   bounding box. */
.pw-line-fill-wave { display: none; transform-box: view-box; }
:root[data-whimsy="0"] .pw-line-fill-line { display: none; }
:root[data-whimsy="0"] .pw-line-fill-wave {
  display: block;
  /* One period is 2 * WAVE_HALF = 14 user units, and sizeSeekWave makes a user
     unit a pixel and lays the path two periods past the edge - so translating by
     one period and looping is seamless. Keep the 14 in step with WAVE_HALF. */
  animation: seek-wave 3s linear infinite;
}
/* Paused: the scroll stops, and at the soft end the wave flattens to the straight
   line - the same swap #nowplaying makes (chrome.css), which this scrubber was
   drawn to match and did not have. Swapping the wavy fill for the straight one
   rather than scaling the wave to nothing, for the reason stated there: a
   scaled-to-zero group leaves no line at all on some engines, and the played part
   of the scrubber then vanishes on pause.

   .is-paused is on the transport, not on the line, because the line is the same
   element in both homes and what is paused is the player. */
.pw-player.is-paused .pw-line-fill-wave { animation-play-state: paused; }
:root[data-whimsy="0"] .pw-player.is-paused .pw-line-fill-wave { display: none; }
:root[data-whimsy="0"] .pw-player.is-paused .pw-line-fill-line { display: block; }
@media (prefers-reduced-motion: reduce) {
  :root[data-whimsy="0"] .pw-line-fill-wave { animation: none; }
}
.pw-controls { display: flex; align-items: center; justify-content: center; gap: 6px; }
.pw-btn {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  /* --radius-pill, per tokens.css:354: these are square icon buttons and follow
     the corner axis. .pw-play below is the exception and puts its 50% back - a
     play button is a disc, and the disc is what says "play" before the glyph
     inside it is read. */
  border-radius: var(--radius-pill);
  background: none;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
@media (hover: hover) {
  .pw-btn:hover { background: var(--paper-2); color: var(--accent-deep); }
}
/* :not(.pw-play) on both - the play button is the one filled control in the row
   and a paper wash over it would undo the fill rather than deepen it. */
.pw-btn:not(.pw-play):active { background: var(--paper-3); }
.pw-btn:not(.pw-play):focus-visible { background: var(--paper-2); color: var(--accent-deep); }
.pw-btn.is-on { color: var(--accent-text); }
/* Flex the glyph wrapper so the SVG is laid out as a flex item, not an inline box:
   an inline SVG rides the text baseline and its descender space nudges every icon a
   couple of pixels up, which read as the play circle sitting low against the row. */
.pw-btn .pw-ico { display: flex; }
.pw-btn svg { display: block; width: 16px; height: 16px; }
.pw-play {
  width: 44px;
  height: 44px;
  border: var(--hairline) solid var(--accent-text);
  border-radius: 50%;
  color: var(--accent-text);
}
@media (hover: hover) {
  .pw-play:hover { background: var(--accent); color: var(--accent-fg); }
}
.pw-play svg { width: 18px; height: 18px; }
/* The two ends of a list being read straight through. Only ever set with repeat
   off and shuffle off - see refresh() in ui/playlist.ts, which is where the
   argument for that lives. It has to *look* dead, because it is: the press does
   nothing rather than wrapping to the other end of the list. */
/* --ink-3 and nothing else, the app's one disabled recipe. The `opacity: 0.4`
   that used to sit on top of it faded the ground as well as the ink, which on a
   row of filled transport buttons cut a hole rather than going quiet. */
.pw-btn:disabled { color: var(--ink-3); cursor: default; }
@media (hover: hover) {
  .pw-btn:disabled:hover { background: none; color: var(--ink-3); }
}

/* "4 of 12", under the artist. A third line in the info column rather than
   something in the seek row, because it is a fact about the *list* and the two
   times either side of the scrubber are facts about the track. */
.pw-pos {
  font-size: var(--t-tiny);
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.pw-pos[hidden] { display: none; }

/* There is one transport and it is the Desktop window's, so there is no lens
   scope under here any more. The lens carried a copy for a while - the same
   `pw-` classes laid out with the room a phone-width sheet has - and it is gone
   with the panel it was in: on the phone the now-playing bar is already at the
   foot of the screen and already the transport for every other surface, and a
   second one on the lens was a thing to scroll past on the way to the music.
   ui/playlist.ts builds a player for the window variant only, and chrome.css no
   longer hides #nowplaying under data-feed-lens. */

/* Reduced motion: no tile reflow easing while reordering, no scroll glide, and a
   still equalizer (the row still lights, it just does not dance). */
@media (prefers-reduced-motion: reduce) {
  #mobile-feed, #mobile-playlist { scroll-behavior: auto; }
  .pl-eq i { animation: none; transform: scaleY(0.7); }
  /* A still tint rather than a travelling sheen - the placeholder still reads. */
  .feed-tile.is-loading::before { animation: none; }
}

@media (pointer: coarse) {
  :root {
    --chrome-button-w: var(--chrome-button-touch);
    --chrome-button-h: var(--chrome-button-touch);
    /* Still not 44px: four of these sit on the corners of a rectangle you are
       looking *through*, so a target that size would hide a card each. Grown to
       where a finger can find it without the sheet becoming mostly handle. */
    --sheet-grip: 20px;
  }

  /* Marks keep their drawn size and the targets around them grow. A finger is
     not a pointer: 9px of slop each way on the edge strips, and a corner reach of
     40 past the ring rather than 24, so a thumb finds the corner without a
     magnifying pinch. The extra reach is spent outward and costs the card
     nothing: what a corner laps back over the face is capped at an eighth of the
     side either way (--grip-lap-x / -y), so 40 does not cover a small card the
     way it used to - it only makes the space outside it easier to hit. */
  /* The edge strips are pure hit area, so --grip-edge is the target itself -
     and it sets the offset as well as the thickness, so a wider strip grows
     away from the card rather than over it. */
  .item { --grip-slop: 9px; --grip-corner: 40px; --grip-edge: 24px; }
  /* Nothing to hover with, and a swollen mark under a finger is a mark under a
     finger. It stays for the drag, which is the part you can see. */
  .grip:not(.grip-edge):hover { --grip-swell: 1; }

  /* The chrome's hover reactions used to be cancelled here, and the
     cancellation is gone rather than guarded a third time.

     What it was for is real. Every hover reaction fires on the *first* tap on a
     touch screen and then sticks until something else is tapped, so a bin
     button stays lit long after you have finished with it. What the rule could
     never do is find out whether it was talking to a finger, and it asked twice
     and got the wrong answer twice. First `pointer: coarse`, which describes
     the machine's primary pointer: a laptop with a touchscreen reports coarse
     in tablet mode with a mouse sitting right there, so the menu button, the
     bin, the zoom pill and the sidebar's own close all went inert under a real
     cursor. Then a nested `any-hover: none`, which is supposed to be true only
     where nothing at all can hover - and Chromium answers it on an ordinary
     Windows desktop. Measured, not guessed: that browser reports `pointer:
     coarse`, `any-pointer: fine` false and `any-hover: none` in every tab and
     on every document, while navigator.maxTouchPoints says 0 and no touch
     screen exists. The whole `pointer: coarse` block applies there, which is
     visible without devtools - the chrome buttons come out at their 46px touch
     size on a full-width window - and every reaction in the list was dead.

     ui/flyout.ts made the same call for the same reason and its header is worth
     reading: the hardware query is the wrong shape. What matters is the pointer
     in your hand right now, which is a question about the event rather than
     about the machine, and CSS cannot ask it - so it stops asking. What is left
     is the `:active` rule below, which this block always agreed was the half
     that carries information rather than decoration. The cost of leaving is
     that on a real touch device the decorative fill lingers until the next tap.

     The tell was the hamburger. This list restored the button's fill and its
     `color`, but the three bars are spans painted with `background`
     (sidebar.css), which the list never named - so on Chromium the bars tinted
     to the accent and the sheet under them did not move. Half a hover is how a
     rule that was quietly wrong on every control got noticed on one. */
  #menu-btn:active, #mobile-header-edit-btn:active, #drawer-btn:active, #zoom-ctl button:active,
  .btn-row button:active { background: var(--paper-3); }

  /* 44px, everywhere a finger has to land. This is the *input* question, so it
     is here rather than in the width query below: a 10" tablet has all the room
     in the world and the same finger. */
  /* 21, not 20: the 45px button over a 1px border is a 43px box, so an odd bar
     centres on a whole pixel. The gap is untouched - the pitch of 8 is the one
     number in this control that must not move. */
  #menu-btn span { width: 21px; }
  #mobile-header-edit-btn svg { width: 20px; height: 20px; }
  #drawer-btn svg { width: 20px; height: 20px; }
  /* No size here: the pill's segments read --chrome-button-w and -h, which
     this query has already taken to --chrome-button-touch. */
  #zoom-ctl svg { width: 17px; height: 17px; }
  .check input { width: 20px; height: 20px; }

  /* A long press on the board opens the context menu (canvas/input.ts). iOS
     answers the same gesture with its own callout - "Copy / Look Up" over a
     card - so the platform's has to be turned off or the two fight. */
  #world, #viewport { -webkit-touch-callout: none; }
}

@media (max-width: 700px) {
  /* Two bands instead of four corners.

     The foot is for the hand: the add bar along the very bottom, with the
     drawer, undo and redo on the row above it. The view controls go up to the top
     edge, because they are the odd one out - zoom is something you set and
     then leave, not something you reach for mid-gesture, and it was the piece
     most often pressed by accident while dragging an item towards the bottom
     of the screen. The menu shares that top edge from the other corner, for
     the reason written over its own rule below.

     Nothing moves in the markup - these are four independently fixed elements
     and always were, so this is only a question of where each one is pinned. */
  /* One row along the foot, not two bands. The bin holds the left corner and
     undo/redo the right, as before, but the toolbar's handle no longer owns the
     whole bottom edge with the pair stepped up over it - it fills the span
     between them instead, so bin, handle and history read as one flush strip.

     --bar-h is the height the three share; --bar-gap is the space the handle
     leaves on each side. Its left inset clears one chrome button (the bin),
     its right inset clears two (the history pair). One place to change the
     height, so the strip cannot drift apart the way the old four corners did.

     --tools-h is the tier the handle opens into. Named beside the other two
     because the player's bottom is worked out from all three and a literal
     there would be a measurement of a rule somewhere else.

     Both heights are the chrome button's own size, so the bin and each history
     button on the strip are square: 45 on touch, 42 with a mouse.

     --panel-inset (sidebar.css) takes this width's 12px, so a panel that is
     still a sheet at the side, from 601px to here, starts level with the
     view controls and the buttons in the top corners. */
  :root { --bar-h: var(--chrome-button-h); --bar-gap: 8px; --tools-h: var(--chrome-button-h); --panel-inset: 12px; }

  /* The toolbar stops being a strip at the top and becomes a drawer at the
     foot. Nothing about the markup changes - it is one fixed element and always
     was - so this is only a question of where it is pinned and which of its two
     halves is drawn.

     The container takes the handle's shape and the tools tier floats out of it
     upward, absolutely positioned against it, which is what keeps the two in
     step: the tier's own left and right are the container's, so it can never
     drift off the bin at one end or the history pair at the other. */
  #toolbar {
    top: auto;
    left: calc(max(12px, env(safe-area-inset-left)) + var(--chrome-button-w) + var(--bar-gap));
    right: calc(max(12px, env(safe-area-inset-right)) + 2 * var(--chrome-button-w) + var(--bar-gap));
    bottom: calc(max(12px, env(safe-area-inset-bottom)) + var(--dock));
    /* The desktop's room (chrome.css) does not apply: the handle's own insets are the room. */
    max-width: none;
    translate: none;
    height: var(--bar-h);
    /* The tier hangs outside the container, so the overflow clip the desktop bar
       uses to keep its segments inside one border would cut it off. */
    overflow: visible;
  }
  /* The Playlist draws no bin and no history (the rule at the top of this
     file), so the handle has the foot to itself and runs edge to edge, under a
     player that does the same. Clearing two buttons that were not there left
     it a stub between two gaps, narrower than the player standing on it. */
  :root[data-board-mode="mobile"][data-feed-lens="playlist"] #toolbar {
    left: max(12px, env(safe-area-inset-left));
    right: max(12px, env(safe-area-inset-right));
  }
  :root[data-board-mode="mobile"][data-feed-lens="playlist"] #toolbar-tools {
    left: 0;
    right: 0;
  }
  /* And the list takes the same width, so sheet, player and handle make one
     column with one pair of edges. The cap is the desktop's, where the column
     is a sleeve in the middle of the room; here the room is the column.
     Counted from the window and not from the scroller (100%), whose width is
     short by the two scrollbar gutters on a desktop: it is centred on the same
     line either way, and the bars are measured from the window. */
  #mobile-playlist .feed-sheet { width: calc(100vw - 24px); }
  #mobile-playlist {
    /* The foot is the handle, then the player standing on it: the last row
       must scroll clear of both (the player's own `bottom` below). */
    --feed-inset-bottom: calc(calc(max(12px, env(safe-area-inset-bottom)) + var(--dock))
                              + var(--bar-h) + var(--bar-gap) + 78px);
  }
  /* Two ids, to match the rule in chrome.css that takes it away. This one
     happened to work at (1,0,0) only because `#toolbar button` also says
     `display: flex`, which is a tie nobody chose and one that would break the
     day that block's display value changed. */
  #toolbar #toolbar-toggle {
    display: flex;
    flex: 1;
    height: 100%;
    /* The corner the bar's own `overflow: hidden` draws on a desktop, drawn by
       the button itself here - because the container above has to be
       `overflow: visible` for the tier to hang outside it, and an unclipped
       :active fill is a square of --paper-3 painted over a rounded bar. It only
       showed on the press, which is why it read as the button being the wrong
       shape rather than the clip being gone.

       `inherit` and not a repeat of --leaf: the toggle is the only child and
       fills the bar exactly, so its corner *is* the bar's corner - and that
       radius is struck from --radius now (see research/old/visual-audit-2026-08-12.md),
       so a second literal here would drift the day the slider moves. */
    border-radius: inherit;
  }

  /* The tier. Its own card rather than part of the handle's - it comes and goes
     while the handle stays, and two objects that appear at different times are
     two objects. Same paper, same corner and same hairline as everything else
     along this edge, for the reason the player's own rule gives.

     Laid out with the same rise the player uses: up from under its own edge,
     not a fade in place. A drawer that materialises where it will sit says
     nothing about where it came from, and this one came from the button
     directly below it. */
  #toolbar-tools {
    position: absolute;
    /* Wider than the handle it opens from, out to the width of the whole foot:
       the tier's edges land on the bin's outer edge at one end and the redo
       button's at the other, which is as wide as anything on this layout gets.
       The handle has to clear those three buttons because it shares their row;
       the tier is a row of its own and clearing them buys nothing but a
       narrower strip - and the width is what pays for a word on a tool.

       Written as the negative of the container's own insets, out of the same
       two tokens, so the tier cannot drift off the strip below when either
       changes. */
    left: calc(-1 * (var(--chrome-button-w) + var(--bar-gap)));
    right: calc(-1 * (2 * var(--chrome-button-w) + var(--bar-gap)));
    bottom: calc(100% + var(--bar-gap));
    height: var(--tools-h);
    border: var(--hairline) solid var(--rule-2);
    border-radius: var(--leaf);
    overflow: hidden;
    background: var(--paper-card);
    box-shadow: var(--shadow-1);
    opacity: 0;
    pointer-events: none;
    visibility: hidden;
    transform: translateY(8px) scale(0.98);
    transition: opacity var(--dur-fast) var(--ease),
                transform var(--dur-fast) var(--ease),
                visibility 0s var(--dur-fast);
  }
  #toolbar.is-open #toolbar-tools {
    opacity: 1;
    pointer-events: auto;
    visibility: visible;
    transform: none;
    transition: opacity var(--dur-base) var(--ease),
                transform var(--dur-base) var(--ease-back),
                visibility 0s;
  }
  /* Every tool takes an equal share of the row, so the tier is one strip of
     even segments rather than a row sized by how long each word happens to be. */
  #toolbar-tools button { flex: 1; padding: 0 6px; height: 100%; }
  /* The tier keeps its words at every width now, which it could not do while
     there were four or five of them on it. Four segments of a 390-wide screen
     came out at ninety points each and the longest label wanted about eighty - a
     13px word, the seventeen-point glyph, the gap and the padding either side -
     so below 360 the words came off all but Files, and that was the tier
     apologising for being too full.

     Three segments of the same screen is a hundred and thirty each, and the
     three labels left are four letters apiece. The rule that hid them is gone
     rather than raised: what it was working around was the tool count, and the
     tool count is what changed. Only the wrap guard stays, because a label that
     breaks in two is a different failure from a label that does not fit. */
  #toolbar-tools button span { white-space: nowrap; }
  /* Rearrange and Find are not here. Both are one tap away in the panel on this
     layout, and a row of six on a 390-wide screen is a row you cannot aim at -
     the same "absent rather than greyed out" the settings schema applies to its
     own layout-specific rows. */
  #toolbar [data-desktop] { display: none; }
  /* And Colour, Link and Stickers step behind More, which is the same argument
     carried one further: four creation tools still came out at ninety points
     each, which is where the words started going. Three segments and a menu
     leaves every one of them a label, and the menu can afford longer ones than
     the bar ever could.
     The pair below are one swap, so they are written together: the three go and
     the one that holds them arrives. */
  #toolbar [data-more] { display: none; }
  #toolbar [data-mobile] { display: flex; }

  /* Above the foot strip rather than in it. The bottom edge is spoken for at
     this width - bin, handle and history read as one flush line - and a fourth
     object squeezed into it would break the thing that makes those three read
     as one. So the player stands off it by the same gap the strip uses inside
     itself, full width between the same insets, and the two stack.

     The name goes with it. There is no room for a filename, a waveform, a
     volume slider and a close on a phone, and of those four the name is the one
     the card itself already carries - you started this from somewhere.

     The gap itself is not set here. --foot-left / --foot-right are defined once
     at :root in chrome.css and changed for both foot bars together wherever
     they are changed; the phone-width pair lives there beside the others. This
     block keeps what is about the player alone. */
  #nowplaying {
    bottom: calc(calc(max(12px, env(safe-area-inset-bottom)) + var(--dock)) + var(--bar-h) + var(--bar-gap));
    --np-w: calc(100vw - var(--foot-left) - var(--foot-right));
  }
  /* And a third tier while the drawer is out, because the tools took the
     second. This is the general sibling combinator, which is the whole reason
     #toolbar has to precede #nowplaying in index.html - the note is repeated
     there, over the markup, where somebody moving an element would read it.

     It moves rather than fades: the player is playing, and a transport that
     vanishes because a drawer opened over it sounds like something that cannot
     be stopped. Same objection the idle rule answers in chrome.css. */
  #toolbar.is-open ~ #nowplaying {
    bottom: calc(calc(max(12px, env(safe-area-inset-bottom)) + var(--dock))
                 + var(--bar-h) + var(--bar-gap)
                 + var(--tools-h) + var(--bar-gap));
  }
  /* The name stays now: the seek line moved to its own row, so the top row has
     room for the name on the left even on a phone (it ellipsises if the controls
     crowd it). */
  #nowplaying .np-volume input { width: 48px; }

  /* And here the sidebar does take it away, unlike at every wider size. The
     panel is 322 of a 390-wide screen: there is no "beside it" to move to, and
     a player peeking out from under an open panel is not a control, it is a
     sliver. The rule above that keeps it up is written for a sidebar that is a
     left-hand strip, which is what it stops being here. */
  :is(#sidebar, #header-panel).is-open ~ #nowplaying {
    --foot-left: max(12px, env(safe-area-inset-left));
    --foot-right: max(12px, env(safe-area-inset-right));
    bottom: calc(calc(max(12px, env(safe-area-inset-bottom)) + var(--dock)) + var(--bar-h) + var(--bar-gap));
    opacity: 0;
    pointer-events: none;
  }

  /* The menu goes back to the top left, which is where it is at every other
     width. It came down here to be reachable by a thumb, and the row it came
     down into has since filled up: the bin brought undo and redo with it, and
     three controls plus the menu at the two far ends of the same strip is a
     row you have to aim at. The panel it opens is a two-handed job anyway -
     you go to it to rename a board or change the look, not mid-gesture - so it
     is the one thing in the corner that can afford the reach. */
  #menu-btn {
    top: max(12px, env(safe-area-inset-top));
    left: max(12px, env(safe-area-inset-left));
  }
  #mobile-find-btn {
    top: max(12px, env(safe-area-inset-top));
    right: max(12px, env(safe-area-inset-right));
    left: auto;
  }
  #mobile-header-edit-btn:not([hidden]) ~ #mobile-find-btn {
    right: calc(max(12px, env(safe-area-inset-right)) + var(--chrome-button-w) + 8px);
  }
  #mobile-header-edit-btn {
    top: max(12px, env(safe-area-inset-top));
    right: max(12px, env(safe-area-inset-right));
    left: auto;
  }

  /* The drawer button takes the left corner of the foot, which is the one it
     holds at every other width. It sat on the right here for a while, from back
     when the menu button owned the bottom left and the two could not share the
     strip; the menu has since moved, so the only thing the right-hand pin still
     bought was a control that changes sides between a phone and the laptop it
     syncs with. */
  #drawer-dock {
    bottom: calc(max(12px, env(safe-area-inset-bottom)) + var(--dock));
    left: max(12px, env(safe-area-inset-left));
  }
  /* And undo and redo stay at the other end, which is the one thing about this
     row that does not match the desktop. On a laptop they sit against the drawer
     because they are the same thought - the drawer is the one mistake you can
     walk back by hand and these are every other one - and the pointer costs
     nothing to move between them. Here the cost is a thumb: both far corners of
     the foot are comfortable and the middle is the stretch, so a pair pressed as
     often as these belongs in a corner rather than beside one - and the add bar
     takes the stretch between them, where it is aimed at rather than reached for
     in a hurry. It stays a child of #drawer-dock, so the idle fade and the
     sidebar-open hide still take it with everything else; only where it is
     pinned changes. */
  #history-ctl {
    position: fixed;
    bottom: calc(max(12px, env(safe-area-inset-bottom)) + var(--dock));
    right: max(12px, env(safe-area-inset-right));
  }
  /* All three share one height so the foot is a single flush line. */
  #drawer-btn, #history-ctl { height: var(--bar-h); }

  /* The toast rises from the foot on a desktop, where the foot is empty. Here
     the foot is the button row, so a line landing at its usual 24px would sit
     behind the drawer button and the add bar. Lift it clear: the row's own
     bottom, plus the row height, plus a gap. */
  #toast { bottom: calc(calc(max(12px, env(safe-area-inset-bottom)) + var(--dock)) + var(--bar-h) + 12px); }
  /* The window is narrowed and lifted clear of the button row it opens from -
     on a desktop it stands on the corner, and here that corner is the foot bar.
     It still hangs off the left edge, so nothing about which way it opens
     changes, and it is still draggable off wherever this leaves it. */
  #drawer-window {
    width: min(360px, calc(100vw - 24px));
    max-height: min(60vh, 460px);
    left: max(12px, env(safe-area-inset-left));
    bottom: calc(calc(max(12px, env(safe-area-inset-bottom)) + var(--dock)) + var(--bar-h) + 10px);
  }
  /* And it gets out of the way almost completely while a card is being carried
     out of it, where a desktop leaves it at 30%.

     The difference is how much of the board it is standing on. On a laptop the
     window is a fifth of the screen in a corner and 30% is enough - you can see
     what you are aiming at past it, and it stays present enough to aim *away*
     from, which matters because a drop let go over the window is a cancel. Here
     it is most of the board: at 30% the phone shows a card being dragged across
     a grey panel, and the thing being aimed at is underneath the thing doing the
     aiming.

     1%, not 0. It has the same job to do at either number - it still takes the
     pointer, so releasing over it still cancels rather than flinging the card
     somewhere arbitrary - and a window at 0 that silently eats a drop is a
     control that has stopped existing while still deciding what happens. One
     percent is a pane of glass you can just make out the edge of. */
  #drawer-window.is-lifting { opacity: 0.01; }

  /* Up at the top edge, clear of everything the thumb is doing. Right-aligned
     rather than centred: #corner is a column that grows downwards from its
     pin, and the top right is the one part of the screen nothing else claims. */
  #corner {
    top: max(12px, env(safe-area-inset-top));
    bottom: auto;
    right: max(12px, env(safe-area-inset-right));
  }
  /* The ruler goes under the view controls here rather than over them, and it
     turns over with the move.

     Both halves are the same decision. #corner grows downwards from a pin at
     the top of the screen at this width, where on a desktop it grows upwards
     from the bottom - so what was the outside edge of the group is now the
     inside one, and a stick drawn with its baseline at the bottom and its
     serifs rising into the pill above reads as a bracket around the wrong
     thing. Turned over, the baseline is the edge it shares with the controls,
     the serifs hang from it, and the number sits at the foot: controls, rule,
     reading, which is the order a scale bar has under a drawing. */
  #corner { flex-direction: column; }
  #scale-bar { order: 1; flex-direction: column-reverse; padding: 2px 2px 0; }
  #scale-bar-stick::before { bottom: auto; top: 0; }

  /* The coordinate readout is the first thing to go. It is a thing to glance
     at while working with a mouse, and there is no cursor to report. */
  #hud { display: none; }

  /* The top edge is clear again - the toolbar went to the foot - so the waiting
     strip takes it back. See the note on #busy in status.css. */
  #busy { top: max(14px, env(safe-area-inset-top)); }

  /* Under the view controls rather than beside them: at this width a centred
     330px strip and a right-pinned zoom bar are the same piece of screen. */
  #busy { top: calc(max(12px, env(safe-area-inset-top)) + 46px); }

  /* A flick that runs off the end of the panel would otherwise carry on into
     the page behind it, which on a phone is the browser's own overscroll. */
  .side-body { overscroll-behavior: contain; }

  /* The find palette is a desktop-width strip; here it wants the width it can
     get, and to sit higher up so the keyboard does not cover its own results. */
  #search {
    top: max(10px, env(safe-area-inset-top));
    left: 10px;
    right: 10px;
    width: auto;
    /* The centring is a `translate`, not a `transform` - cancelling the wrong
       one of those leaves the palette half a screen to the left. */
    translate: none;
  }
}

/* The panel fills the screen from 600px down, not from 700px with the rest of
   the phone layout. Between the two it stays the sheet at the side
   (sidebar.css): 322px of it leaves a board beside it worth seeing, and a
   whole screen of panel there felt too early. ui/sidebar.ts asks the same
   width in panelFillsScreen(). */
@media (max-width: 600px) {
  /* And with the panel open, the view controls go too - so nothing is left on
     the glass but the panel.

     Only at this width, and that is the whole of the reasoning. On a desktop
     the sidebar is a sheet laid over one edge of a board you can still see and
     still use, so the zoom controls in the far corner are as live as they ever
     were. Here the panel *is* the screen: the board behind it is not visible,
     the scale bar is measuring something nobody can look at, and a zoom control
     floating over a full-screen panel reads as a piece of the panel - one that
     does nothing you would expect a control on this screen to do. The menu
     button, the drawer and the toolbar already leave for exactly this reason; this
     is the last corner that stayed.

     visibility with the opacity, and delayed by the fade's own length, so the
     controls finish fading before they leave the tab order and come back the
     instant the panel starts to close. The same pairing #drawer-dock and #toolbar use -
     a control faded to nothing that can still be pressed is worse than one that
     is simply there. */
  #sidebar.is-open ~ #corner,
  #header-panel.is-open ~ #corner {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--dur-base) var(--ease), visibility 0s var(--dur-base);
  }

  /* The masthead pen leaves with the rest when the sidebar becomes the screen.
     In sidebar.css it rides over the sidebar, which is right wherever the
     sidebar is a strip at the side and the pen's own corner stays clear - but
     here the panel is full-screen, so a pen floating on top of it is a control
     pointing at a masthead nothing on this view is showing. */
  #sidebar.is-open ~ #mobile-header-edit-btn {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity var(--dur-base) var(--ease), visibility 0s var(--dur-base);
  }

  /* The panel becomes the screen rather than a sheet laid on it. At this width
     the board behind it is not usable anyway, and half a panel is worse than a
     whole one. So it runs edge to edge, over the timeline at the foot, with no
     border, rounding or shadow: those are what make a sheet read as laid on the
     screen. The notch and the home indicator become padding inside it, so
     neither eats anything. The sidebar's foot keeps clear of the home indicator
     already (sidebar.css); the header panel has no foot and takes it here.

     It used to stop 58px short of the bottom to leave the zoom bar showing.
     That was the wrong trade twice over: it spent the scarcest thing on a phone
     - vertical room in a panel that is mostly a scrolling list - on a strip of
     controls that do nothing while the panel is up, and it left a band of live
     board at the foot of a screen you are scrolling with your thumb. */
  #sidebar,
  #header-panel {
    inset: 0;
    width: auto;
    padding: env(safe-area-inset-top) env(safe-area-inset-right) 0 env(safe-area-inset-left);
    border: 0;
    border-radius: 0;
    box-shadow: none;
    /* Just a fade here. The slide-with-overshoot is a sheet arriving from off
       to the left, which reads only while there is board either side of it to
       arrive over; at this width the panel *is* the screen, so the same motion
       is the whole view lurching sideways. Closed state keeps the transform at
       none so nothing is being animated but the opacity. */
    transform: none;
    transition: opacity var(--dur-base) var(--ease),
                visibility 0s linear var(--dur-base),
                background-color var(--dur-fast) var(--ease),
                border-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
  }
  #header-panel { padding-bottom: env(safe-area-inset-bottom); }

  /* A range and its own row - name, value, stop names - are the only visible
     things while a finger is tuning it. The panel itself dissolves as well as
     its controls, exposing the board beneath while the active track stays
     exactly where the gesture began. Ancestors of that input remain laid out
     but lose their own decoration; every other branch fades completely.
     Releasing the pointer removes the state immediately.
     Here and not from 700px: a panel at the side leaves the board in view, and
     there the dissolve only hid the panel. createMobileSliderFocus() in
     ui/sidebar.ts asks the same width. */
  :is(#sidebar, #header-panel).is-slider-focus {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
  }
  :is(#sidebar, #header-panel).is-slider-focus .side-head,
  /* The tab strip goes wholesale, like the head and the foot and unlike the
     body: no slider is ever inside it, so it needs none of the
     :not(:has(.is-slider-active)) care the rows below it do. */
  :is(#sidebar, #header-panel).is-slider-focus .side-tabs,
  :is(#sidebar, #header-panel).is-slider-focus .side-foot,
  :is(#sidebar, #header-panel).is-slider-focus .side-body :where(*):not(:has(.is-slider-active)):not(.is-slider-active):not(.field:has(.is-slider-active) *) {
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--dur-fast) var(--ease);
  }
  /* The scrollbar goes with everything else in the panel while a slider is
     being dragged. Written as the thumb rather than as scrollbar-color, which
     is the app's own bar everywhere now (base.css): the standard property
     would not have hidden the drawn one, it would have replaced it with the
     operating system's, which is the opposite of what this rule wants. */
  :is(#sidebar, #header-panel).is-slider-focus .side-body::-webkit-scrollbar-thumb {
    background: transparent;
  }
  :is(#sidebar, #header-panel).is-slider-focus .side-body :where(*):has(.is-slider-active) {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
  }
  /* The row keeps a card of its own, since a bare track over the board says
     neither what it sets nor where it has got to. Drawn as a spread shadow and
     not padding, which would move the track out from under the finger. Two
     spreads and no elevation token: a tier sets --shadow-2 to none, and none
     inside a list drops the whole declaration. Beats the strip above on
     specificity, not order. */
  :is(#sidebar, #header-panel).is-slider-focus .side-body .field:has(.is-slider-active) {
    background: var(--paper-card);
    border-radius: var(--radius);
    box-shadow: 0 0 0 10px var(--paper-card), 0 0 0 11px var(--rule);
    transition: background-color var(--dur-fast) var(--ease),
                box-shadow var(--dur-fast) var(--ease);
  }
  :is(#sidebar, #header-panel).is-slider-focus input[type="range"].is-slider-active {
    position: relative;
    z-index: 2;
    opacity: 1;
    pointer-events: auto;
  }
}

/* Landscape on a phone: the bar is still at the bottom but there is barely any
   bottom to put it in, so the panel's head is tighter. */
@media (max-width: 900px) and (max-height: 480px) {
  .side-head { padding: 10px 14px 8px; }
  /* The close button's top keeps level with its side: 10 + 4 = 14 (R35). */
  :is(#side-close, #header-close) { margin-top: 4px; }
  /* Wider than 600px, the panel is still a sheet at the side, and it gives up
     the inset to take the height. Narrower, it is the whole screen already. */
  @media not all and (max-width: 600px) {
    #sidebar, #header-panel { top: 6px; bottom: 6px; }
  }
}

/* Without color-mix(), for WebKit below 16.2 - section 7 of tokens.css carries
   the argument and declares the channel-form tokens.

   The heaviest block in the app, because the phone chrome tints more than
   anything else does: a feed tile, a playlist row and a cover placeholder are
   all the accent laid over the sheet at some share, and that is a two-colour
   blend rather than a colour at an alpha. Sixteen of the twenty-four here are
   blends and none of them can be written down - Harsh derives the sheet itself
   from --accent, and --accent can be a pigment pulled out of a photograph. They
   read --lc-* tokens that legacyBlends() in ui/appearance.ts computes and
   rewrites whenever the look changes.

   Every one carries a var() fallback, and the fallbacks are chosen so that a
   board with no script yet is quiet rather than wrong: a tint falls back to the
   sheet it was a tint of, and a tinted ink falls back to the ink. Nothing here
   falls back to nothing, because most of these are backgrounds on chrome that
   has to stay legible over the board. */
@supports not (color: color-mix(in srgb, red, blue)) {
  /* Alpha only - exact, and needing nothing from script. */
  .feed-play { background: rgb(0 0 0 / 18%); }
  .pl-eq { background: rgb(0 0 0 / 34%); }
  .feed-tile-badge { background: rgb(var(--paper-c) / 82%); }
  .feed-swatch-label { background: rgb(var(--paper-c) / 78%); }
  .pl-cover::after {
    box-shadow: inset 0 0 0 var(--hairline) rgb(var(--ink-c) / 12%);
  }
  .pw-line-base { stroke: rgb(var(--ink-c) / 20%); }
  .pl-disc {
    background:
      radial-gradient(circle closest-side at 50% 50%,
        var(--paper-card) 0 5%, transparent 5.5%),
      radial-gradient(circle closest-side at 50% 50%,
        var(--accent) 5.5% 33%, transparent 33.5%),
      repeating-radial-gradient(circle closest-side at 50% 50%,
        rgb(var(--ink-c) / 86%) 0 1.5px,
        rgb(var(--ink-c) / 70%) 1.5px 3.5px);
  }

  /* Blends - computed in script, with the sheet or the ink behind each. */
  #mobile-feed,
  #mobile-playlist { background: var(--lc-ink-6-paper, var(--paper)); }
  .feed-sheet { background: var(--lc-accent-6-white, #fff); }
  .feed-tile[data-kind="audio"] {
    background: var(--lc-accent-10-paper-3, var(--paper-3));
  }
  .feed-tile-art.is-placeholder,
  .pl-row.is-playing .pl-time { color: var(--lc-accent-70-ink-3, var(--ink-3)); }
  .feed-bars i { background: var(--lc-accent-58-ink-3, var(--ink-3)); }
  .pl-cover,
  .pl-art,
  .pw-cover { background: var(--lc-accent-12-paper-3, var(--paper-3)); }
  .pl-cover.is-placeholder,
  .pw-cover.is-placeholder { color: var(--lc-accent-65-ink-3, var(--ink-3)); }
  .pl-shuffle.is-on { background: var(--lc-accent-14-paper-card, var(--paper-card)); }
  .pl-row.is-playing { background: var(--lc-accent-10-paper-card, var(--paper-card)); }
  .pl-art.is-placeholder {
    background: var(--lc-accent-9-paper-2, var(--paper-2));
    color: var(--lc-ink-2-75-accent, var(--ink-2));
  }
  .pl-row.is-playing .pl-art.is-placeholder {
    background: var(--lc-accent-14-paper-2, var(--paper-2));
  }
}
