/* OMNI app shell — persistent chrome; only #omni-main swaps on nav so <audio> survives.
   Load after styles.css and omni-tokens.css. */

/* 1. GRID
   >=1100px: rail + content (+ optional right rail)
   768-1099: icon rail + content
   <768px: content + bottom tab bar
   Bottom padding always clears the player (and tab bar on mobile). */
body.omni-shell {
  min-height: 100dvh;
  padding-bottom: calc(var(--player-h) + env(safe-area-inset-bottom));
  overflow-x: hidden;
}
#omni-app { display: grid; grid-template-columns: var(--sidebar-w) minmax(0,1fr); max-width: var(--max-w); margin: 0 auto; }
#omni-main { min-width: 0; padding: 18px var(--gutter) 40px; }
#omni-main[data-col="feed"]  > * { max-width: var(--col-feed); margin-inline: auto; }
#omni-main[data-col="wide"]  > * { max-width: var(--col-wide); margin-inline: auto; }
#omni-main[data-col="full"]  > * { max-width: none; }
/* soft-nav swap feel — content fades, chrome never flinches */
#omni-main { animation: omni-in var(--dur-3) var(--ease-out); }
#omni-main[data-swapping="1"] { opacity: .35; transition: opacity var(--dur-1) var(--ease-out); }
@keyframes omni-in { from { opacity:0; transform: translateY(6px);} to { opacity:1; transform:none; } }

/* 2. LEFT RAIL */
#omni-rail {
  position: sticky; top: 0; align-self: start; z-index: var(--z-rail);
  height: 100dvh; padding: 14px 10px calc(var(--player-h) + 14px);
  display: flex; flex-direction: column; gap: 2px;
  border-right: 1px solid var(--line);
}
#omni-rail .rail-mark {
  display:flex; align-items:center; gap:10px; padding:10px 12px 18px;
  font: 800 20px/1 var(--display); letter-spacing:-.02em; color: var(--text);
}
#omni-rail .rail-mark .dot { color: var(--teal); }
.rail-link {
  display:flex; align-items:center; gap:14px; padding:11px 14px;
  border-radius: var(--r-pill); color: var(--text-2);
  font: 600 15px/1 var(--sans); white-space: nowrap;
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out);
}
.rail-link:hover { background: var(--surface-2); color: var(--text); }
.rail-link[aria-current="page"] { color: var(--text); font-weight: 800; background: var(--surface-2); }
.rail-link[aria-current="page"] .ico { color: var(--violet-2); }
.rail-link .ico { width:22px; height:22px; flex:0 0 22px; stroke: currentColor; fill:none; stroke-width:1.9; stroke-linecap:round; stroke-linejoin:round; }
.rail-link .badge-dot { margin-left:auto; width:8px; height:8px; border-radius:50%; background: var(--teal); }
#omni-rail .rail-cta { margin: 12px 6px 0; }
#omni-rail .rail-me { margin-top:auto; display:flex; align-items:center; gap:10px; padding:10px 12px; border-radius: var(--r-pill);
  transition: background var(--dur-2) var(--ease-out); }
#omni-rail .rail-me:hover { background: var(--surface-2); }

/* 3. TOP BAR (mobile) */
#omni-topbar {
  position: sticky; top: 0; z-index: var(--z-rail); height: var(--nav-h);
  display: none; align-items: center; gap: 12px; padding: 0 14px;
  background: var(--glass-bg); backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur); border-bottom: 1px solid var(--line);
}
#omni-topbar .tb-title { font: 800 17px/1 var(--display); letter-spacing:-.01em; }
#omni-topbar .tb-right { margin-left:auto; display:flex; align-items:center; gap:8px; }

/* 4. BOTTOM TAB BAR (mobile) */
#omni-tabbar {
  display: none; position: fixed; left:0; right:0;
  bottom: 0; z-index: var(--z-tabbar);
  height: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  background: var(--glass-bg); backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur); border-top: 1px solid var(--line);
}
#omni-tabbar .tab {
  flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
  color: var(--text-3); font: 700 10px/1 var(--sans); letter-spacing:.02em;
  min-height: 44px; /* touch target */
  transition: color var(--dur-2) var(--ease-out);
}
#omni-tabbar .tab[aria-current="page"] { color: var(--text); }
#omni-tabbar .tab[aria-current="page"] .ico { color: var(--violet-2); }
#omni-tabbar .ico { width:23px; height:23px; stroke:currentColor; fill:none; stroke-width:1.9; stroke-linecap:round; stroke-linejoin:round; }

/* 5. NOW-PLAYING BAR */
#omni-player {
  position: fixed; left:0; right:0; z-index: var(--z-player);
  bottom: 0;
  height: calc(var(--player-h) + env(safe-area-inset-bottom));
  padding: 0 16px env(safe-area-inset-bottom);
  display: grid; grid-template-columns: minmax(0,1fr) auto minmax(0,1fr); align-items:center; gap:16px;
  background: var(--glass-bg); backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  border-top: 1px solid var(--glass-line); box-shadow: var(--glass-shadow);
  transform: translateY(0); visibility: visible;
  transition: transform var(--dur-3) var(--ease-out), opacity var(--dur-2) var(--ease-out), visibility 0s;
}
/* Empty state — never show an empty chrome bar. visibility (not just opacity)
   also takes the hidden controls out of the tab order; it flips only after
   the slide-out finishes, so the animation still reads. */
#omni-player[data-empty="1"] {
  transform: translateY(110%); opacity: 0; pointer-events: none; visibility: hidden;
  transition: transform var(--dur-3) var(--ease-out), opacity var(--dur-2) var(--ease-out),
              visibility 0s linear var(--dur-3);
}
body.omni-shell[data-player="on"] { padding-bottom: calc(var(--player-h) + env(safe-area-inset-bottom)); }

.np-now { display:flex; align-items:center; gap:12px; min-width:0; }
.np-art { width:52px; height:52px; border-radius: var(--r-sm); object-fit:cover; flex:0 0 52px;
  background: var(--surface-3); display:flex; align-items:center; justify-content:center; color: var(--text-3); }
.np-meta { min-width:0; display:flex; flex-direction:column; gap:2px; }
.np-title { font:700 14px/1.2 var(--sans); color: var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.np-artist{ font:500 12px/1.2 var(--sans); color: var(--text-3); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
/* Every control in the bar is a bare <button>. With no explicit reset each one
   keeps the UA button face (#efefef) and a 2px outset border, and the transport
   glyphs are stroked in near-white (--text-2) — near-white on #efefef is a ~1:1
   contrast ratio, so the row reads as five blank discs. Only .np-play escaped it,
   because it paints its own dark glyph on a light disc. Reset the shared chrome
   here; the rules below still set each control's own colour and geometry. */
.np-btn, .np-play, .np-like, .np-expand {
  -webkit-appearance: none; appearance: none;
  background: transparent; border: 0; margin: 0; padding: 0;
  font: inherit; cursor: pointer; -webkit-tap-highlight-color: transparent;
}
.np-like { color: var(--text-3); padding:6px; border-radius:50%;
  transition: color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out); }
.np-like:hover { color: var(--text); background: var(--surface-2); }
.np-like[aria-pressed="true"] { color: var(--teal); }

.np-controls { display:flex; flex-direction:column; align-items:center; gap:6px; min-width: 340px; }
.np-buttons { display:flex; align-items:center; gap:8px; }
.np-btn { width:34px; height:34px; display:grid; place-items:center; border-radius:50%; color: var(--text-2);
  transition: color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out); }
.np-btn:hover { color: var(--text); background: var(--surface-2); }
.np-btn[aria-pressed="true"] { color: var(--violet-2); }
/* center transport carries the row — the filled disc sits ~1.4x its neighbors,
   and the glyph holds half the disc so it does not read lost at the larger size */
.np-play { width:48px; height:48px; border-radius:50%; background: var(--text); color: var(--bg); display:grid; place-items:center;
  transition: transform var(--dur-1) var(--ease-out); }
.np-play:hover { transform: scale(1.06); }
.np-play .ico { width:24px; height:24px; flex:0 0 24px; }
.np-scrub { display:flex; align-items:center; gap:10px; width:100%; }
.np-time { font: 500 11px/1 var(--mono); color: var(--text-3); flex:0 0 38px; text-align:center; }
/* seek bar — also doubles as the SoundCloud waveform host on the music page */
.np-bar { position:relative; flex:1; height:4px; border-radius: var(--r-pill); background: var(--surface-3); cursor:pointer; }
.np-bar .buf  { position:absolute; inset:0 auto 0 0; border-radius: var(--r-pill); background: var(--line-2); }
.np-bar .fill { position:absolute; inset:0 auto 0 0; border-radius: var(--r-pill); background: var(--grad-brand); }
.np-bar .knob { position:absolute; top:50%; width:11px; height:11px; margin:-5.5px 0 0 -5.5px; border-radius:50%;
  background:#fff; opacity:0; transition: opacity var(--dur-1) var(--ease-out); box-shadow: 0 1px 4px rgba(0,0,0,.5); }
.np-bar:hover .knob, .np-bar[data-dragging="1"] .knob { opacity:1; }
.np-bar[data-dragging="1"] { height:6px; }

.np-right { display:flex; align-items:center; justify-content:flex-end; gap:10px; }
.np-vol { width:92px; }
.np-expand { color: var(--text-3); padding:6px; border-radius:50%;
  transition: color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out); }
.np-expand:hover { color: var(--text); background: var(--surface-2); }

/* 6. RESPONSIVE */
@media (max-width: 1099px) {
  :root { --sidebar-w: var(--rail-w-icon); }
  .rail-link { justify-content:center; padding:12px; }
  .rail-link span, #omni-rail .rail-mark span, #omni-rail .rail-me .who { display:none; }
  #omni-rail .rail-cta { display:grid; place-items:center; }
}
@media (max-width: 767px) {
  #omni-app { grid-template-columns: minmax(0,1fr); }
  #omni-rail { display:none; }
  #omni-topbar { display:flex; }
  #omni-tabbar { display:flex; }
  #omni-main { padding: 12px 14px 24px; }
  body.omni-shell {
    padding-bottom: calc(var(--player-h-sm) + var(--tabbar-h) + env(safe-area-inset-bottom));
  }
  /* mini bar sits ON TOP of the tabbar, Spotify-style */
  #omni-player {
    height: var(--player-h-sm);
    bottom: calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    padding: 0 10px;
    grid-template-columns: minmax(0,1fr) auto;
    border-radius: var(--r-md) var(--r-md) 0 0;
    margin: 0 6px;
    left: 0; right: 0;
  }
  .np-controls { min-width:0; }
  .np-scrub, .np-right, .np-vol { display:none; }
  /* progress becomes a 2px hairline across the top edge of the mini bar */
  #omni-player::after {
    content:''; position:absolute; left:0; top:0; height:2px; width: var(--np-progress, 0%);
    background: var(--grad-brand); border-radius: 0 2px 2px 0;
  }
  .np-art { width:44px; height:44px; flex:0 0 44px; }
}

/* 7. FULL-SCREEN NOW PLAYING SHEET */
#omni-np-sheet {
  position: fixed; inset: 0; z-index: var(--z-sheet);
  background: linear-gradient(180deg, var(--surface-2) 0%, var(--bg) 60%);
  transform: translateY(100%);
  display:flex; flex-direction:column; padding: 20px 20px calc(28px + env(safe-area-inset-bottom));
  overscroll-behavior: contain;
  /* closed: off-screen AND out of the tab order — a full-viewport dialog
     whose controls stay focusable would trap the keyboard */
  visibility: hidden; pointer-events: none;
  transition: transform var(--dur-4) var(--ease-out), visibility 0s linear var(--dur-4);
}
#omni-np-sheet[data-open="1"] {
  transform: translateY(0); visibility: visible; pointer-events: auto;
  transition: transform var(--dur-4) var(--ease-out), visibility 0s;
}
#omni-np-sheet .sheet-art { width:min(76vw,420px); aspect-ratio:1; border-radius: var(--r-lg); margin: 6vh auto 24px;
  object-fit:cover; background: var(--surface-3); box-shadow: var(--shadow-pop); }
#omni-np-sheet .sheet-queue { margin-top:auto; }

/* 8. SKELETONS + PULL-TO-REFRESH */
/* the 1.3s loop is ambient pacing, deliberately above the transition scale */
.omni-skel { background: linear-gradient(90deg, var(--surface) 25%, var(--surface-2) 37%, var(--surface) 63%);
  background-size: 400% 100%; animation: omni-shimmer 1.3s var(--ease-inout) infinite; border-radius: var(--r-md); }
@keyframes omni-shimmer { 0%{background-position:100% 0} 100%{background-position:0 0} }
#omni-ptr {
  height: 0; overflow: hidden; display:grid; place-items:center;
  color: var(--violet-2); font: 700 12px/1 var(--sans);
}
#omni-ptr .spin { width:20px; height:20px; border-radius:50%;
  border:2px solid var(--line-2); border-top-color: var(--violet); }
#omni-ptr[data-state="armed"] .spin { transform: rotate(180deg); transition: transform var(--dur-2) var(--ease-out); }
#omni-ptr[data-state="loading"] .spin { animation: omni-spin .7s linear infinite; }
@keyframes omni-spin { to { transform: rotate(360deg); } }

/* focus ring — one ring, everywhere. The rule rounds the outline's corners
   via border-radius, which would also reshape a pill or disc control while
   focused; the two companions restore those controls' own shape so the
   ring follows it instead of squaring it off. */
body.omni-shell :focus-visible { outline: 2px solid var(--violet-2); outline-offset: 2px; border-radius: var(--r-xs); }
body.omni-shell .rail-link:focus-visible,
body.omni-shell .rail-me:focus-visible,
body.omni-shell .omni-timerange button:focus-visible,
body.omni-shell .omni-repost:focus-visible,
body.omni-shell .omni-row-toggle [role="switch"]:focus-visible,
body.omni-shell .omni-hue-reset:focus-visible { border-radius: var(--r-pill); }
body.omni-shell .np-btn:focus-visible,
body.omni-shell .np-play:focus-visible,
body.omni-shell .np-like:focus-visible,
body.omni-shell .np-expand:focus-visible,
body.omni-shell .omni-q-row .q-x:focus-visible,
body.omni-shell .rail-me-edit:focus-visible,
body.omni-shell .omni-hue-sw:focus-visible { border-radius: 50%; }

/* Presentation components. No money logic, labels, or currency formatting. */

/* 9. TIME-RANGE SELECTOR
   Steps: 24h 7d 30d 45d 60d 90d 6mo 9mo 1y 2y 3y 4y 5y.
   Caller supplies the data and the matching date span. */
.omni-timerange {
  display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none;
  padding: 2px 0 10px; -webkit-overflow-scrolling: touch;
}
.omni-timerange::-webkit-scrollbar { display: none; }
.omni-timerange button {
  flex: 0 0 auto; padding: 6px 12px; border-radius: var(--r-pill);
  border: 1px solid var(--line); background: var(--surface-2);
  color: var(--text-3); font: 700 12px/1 var(--sans); white-space: nowrap;
  transition: color var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
}
.omni-timerange button:hover { color: var(--text-2); border-color: var(--line-2); }
.omni-timerange button[aria-pressed="true"] {
  color: var(--text); background: var(--surface-3); border-color: var(--line-hi);
}
/* the matching date span the chart should print under the axis */
.omni-timerange-span { font: 500 11.5px/1 var(--mono); color: var(--text-4); padding: 0 2px 12px; }

/* 10. DELTA — always render percent AND the absolute number.
   Percent alone is not a valid state for this component. */
.omni-delta { display: inline-flex; align-items: baseline; gap: 6px; font: 600 12.5px/1 var(--sans); }
.omni-delta .pct  { font-family: var(--mono); font-weight: 700; }
.omni-delta .abs  { color: var(--text-3); font-size: 12px; }
.omni-delta[data-dir="up"]   .pct { color: var(--ok); }
.omni-delta[data-dir="down"] .pct { color: var(--rose); }
.omni-delta[data-dir="flat"] .pct { color: var(--text-3); }
.omni-delta[data-dir="up"]   .pct::before { content: "▲ "; font-size: 9px; }
.omni-delta[data-dir="down"] .pct::before { content: "▼ "; font-size: 9px; }

/* 11. NUMERIC DISPLAY — renders a bare tabular figure and carries
   NO unit of its own. The caller supplies any label as a separate
   .omni-unit element. */
.omni-num {
  font-family: var(--mono); font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em; font-weight: 700;
}
.omni-num-lg { font-size: 30px; line-height: 1; }
.omni-num-md { font-size: 20px; line-height: 1; }
.omni-num-sm { font-size: 14px; line-height: 1; }
/* the label that sits beside a figure — caller-supplied text only */
.omni-unit { font: 600 11px/1 var(--sans); letter-spacing: .08em; text-transform: uppercase; color: var(--text-3); }

/* 12. PREVIEW BANNER — component only; the caller supplies copy. */
.omni-sim-banner {
  display: flex; align-items: flex-start; gap: 10px;
  padding: 11px 14px; margin: 0 0 16px;
  border-radius: var(--r-md);
  background: var(--orange-dim); border: 1px solid rgba(255,120,73,.32);
  color: var(--orange-2); font: 500 12.5px/1.5 var(--sans);
}
.omni-sim-banner::before {
  content: "PREVIEW"; flex: 0 0 auto;
  font: 800 10px/1.55 var(--sans); letter-spacing: .1em;
  padding: 2px 7px; border-radius: var(--r-xs);
  background: rgba(255,120,73,.22); color: var(--orange-2);
}
.omni-sim-banner strong { color: var(--text); font-weight: 700; }
/* inline variant for a single stat that is preview-only */
.omni-sim-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  background: var(--orange); vertical-align: 2px; margin-left: 5px; }

/* 13. NEXT-ACTION CARDS — sits directly beneath a stat row. */
.omni-actions-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(240px,1fr));
  gap: 12px; margin: 16px 0 24px;
}
.omni-action-card {
  display: flex; align-items: center; gap: 12px; padding: 14px;
  border-radius: var(--r-md); background: var(--surface-2);
  border: 1px solid var(--line); text-align: left;
  transition: border-color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
.omni-action-card:hover { border-color: var(--line-hi); background: var(--surface-3); }
.omni-action-card .glyph {
  width: 34px; height: 34px; flex: 0 0 34px; border-radius: var(--r-sm);
  display: grid; place-items: center; background: var(--violet-dim); color: var(--violet-2);
}
.omni-action-card .txt { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.omni-action-card .t { font: 700 13.5px/1.3 var(--sans); color: var(--text); }
.omni-action-card .s { font: 500 12px/1.3 var(--sans); color: var(--text-3); }

/* 14. SHARE / REPOST ACTION */
.omni-repost { display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 10px; border-radius: var(--r-pill); color: var(--text-3);
  font: 600 12.5px/1 var(--sans);
  transition: background var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out); }
.omni-repost:hover { background: var(--surface-2); color: var(--text); }
.omni-repost[aria-pressed="true"] { color: var(--ok); background: rgba(34,197,94,.12); }
/* a reposted item in the feed gets an attribution line above the card */
.omni-repost-by { display: flex; align-items: center; gap: 7px; padding: 0 0 6px 52px;
  font: 600 12.5px/1 var(--sans); color: var(--text-3); }

/* 15. CARD GRID (music + library) */
.omni-cards {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(168px,1fr)); gap: 16px;
}
.omni-card-art {
  aspect-ratio: 1; border-radius: var(--r-md); position: relative; overflow: hidden;
  background: var(--surface-3); display: grid; place-items: center;
  font-size: 34px; color: var(--text-4);
}
.omni-card-art .play-fab {
  position: absolute; right: 10px; bottom: 10px; width: 42px; height: 42px;
  border-radius: 50%; background: var(--violet); color: var(--brand-ink); display: grid; place-items: center;
  opacity: 0; transform: translateY(6px);
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out);
  box-shadow: 0 6px 18px rgba(0,0,0,.5);
}
.omni-card:hover .play-fab, .omni-card:focus-within .play-fab { opacity: 1; transform: none; }
.omni-card .t { font: 700 14px/1.3 var(--sans); margin-top: 9px; }
.omni-card .s { font: 500 12.5px/1.3 var(--sans); color: var(--text-3); }
@media (max-width: 700px) { .omni-cards { grid-template-columns: repeat(2, 1fr); gap: 12px; } }
@media (max-width: 420px) { .omni-cards { grid-template-columns: 1fr; } }

/* 16. HORIZONTAL SCROLL ROW */
.omni-scroll-row { display: flex; gap: 16px; overflow-x: auto; padding-bottom: 8px; scrollbar-width: none; }
.omni-scroll-row::-webkit-scrollbar { display: none; }
.omni-scroll-row > * { flex: 0 0 180px; }

/* 17. .omni-tabs — underline tab bar.
   Named .omni-tabs, not .tabs: styles.css already defines a global
   .tabs (the auth pill switcher). Do not redefine this per page. */
.omni-tabs {
  display: flex; position: sticky; top: 0; z-index: 10;
  border-bottom: 1px solid var(--line);
  background: var(--glass-bg);
  backdrop-filter: var(--glass-blur); -webkit-backdrop-filter: var(--glass-blur);
  margin: 0 0 14px;
}
.omni-tabs > button, .omni-tabs > a {
  flex: 1; position: relative; padding: 14px 0; text-align: center;
  font: 700 14.5px/1 var(--sans); color: var(--text-3);
  transition: color var(--dur-2) var(--ease-out);
}
.omni-tabs > *:hover { color: var(--text-2); }
.omni-tabs > *[aria-selected="true"], .omni-tabs > *.on { color: var(--text); }
.omni-tabs > *[aria-selected="true"]::after, .omni-tabs > *.on::after {
  content: ''; position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
  width: 56px; height: 3px; border-radius: 3px 3px 0 0; background: var(--grad-brand);
}
/* under the mobile top bar the tabs must stick BELOW it, not under it */
@media (max-width: 767px) { .omni-tabs { top: var(--nav-h); } }

/* 18. NOTIFICATIONS
   Rows whose subject is consent-gated must render the anonymous
   shape by default and be upgraded to a name only when the consent
   source returns one. The anonymous mark (.n-anon) is deliberately
   not an avatar and carries no identifier. */
.omni-notif-list { display: flex; flex-direction: column; }
.omni-notif {
  display: flex; gap: 12px; align-items: flex-start;
  padding: 14px 12px; border-bottom: 1px solid var(--line);
  transition: background var(--dur-2) var(--ease-out);
}
.omni-notif:hover { background: var(--card-hover); }
.omni-notif[data-unread="1"] { background: var(--violet-dim); }
.omni-notif[data-unread="1"]:hover { background: rgba(139,108,255,.18); }
.omni-notif .n-glyph {
  width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%;
  display: grid; place-items: center; background: var(--surface-2); color: var(--text-2);
}
.omni-notif[data-kind="like"]      .n-glyph { background: var(--teal-dim);   color: var(--teal-2); }
.omni-notif[data-kind="repost"]    .n-glyph { background: rgba(34,197,94,.12); color: var(--ok); }
.omni-notif[data-kind="follow"]    .n-glyph { background: var(--violet-dim); color: var(--violet-2); }
.omni-notif[data-kind="support"]   .n-glyph { background: var(--teal-dim);   color: var(--teal-2); }
.omni-notif[data-kind="mention"]   .n-glyph { background: var(--orange-dim); color: var(--orange-2); }
.omni-notif .n-body { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 3px; }
.omni-notif .n-txt { font: 500 14px/1.45 var(--sans); color: var(--text); }
.omni-notif .n-txt b { font-weight: 700; }
.omni-notif .n-when { font: 500 12px/1 var(--sans); color: var(--text-3); }
.omni-notif .n-thumb { width: 42px; height: 42px; flex: 0 0 42px; border-radius: var(--r-xs); object-fit: cover; background: var(--surface-3); }
/* the anonymous supporter face — deliberately NOT an avatar.
   It must never look like a person we could name if you clicked it. */
.omni-notif .n-anon {
  width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%;
  display: grid; place-items: center; background: var(--surface-3);
  color: var(--text-3); font: 700 15px/1 var(--display);
}
.omni-notif-empty { padding: 40px 16px; text-align: center; color: var(--text-3); font-size: 13.5px; }

/* rail / tabbar unread dot */
.rail-link .badge-dot, .omni-tabbar-dot {
  margin-left: auto; width: 8px; height: 8px; border-radius: 50%;
  background: var(--teal); flex: 0 0 8px;
}
.rail-link .badge-count {
  margin-left: auto; min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: var(--r-pill); background: var(--violet); color: var(--brand-ink);
  font: 700 11px/20px var(--mono); text-align: center; flex: 0 0 auto;
}
/* on the icon rail + mobile tab bar the dot rides the glyph */
#omni-tabbar .tab { position: relative; }
#omni-tabbar .tab .dot, .rail-link[data-collapsed] .badge-dot {
  position: absolute; top: 6px; left: 50%; margin-left: 6px;
  width: 8px; height: 8px; border-radius: 50%; background: var(--teal);
}
@media (max-width: 1099px) {
  .rail-link .badge-count { position: absolute; top: 4px; right: 8px; margin: 0; }
  .rail-link { position: relative; }
}

/* 19. MESSAGES / DM SHELL
   Threads render only where identity is already consented on both
   ends. .omni-dm-locked is a first-class state, not an error. */
.omni-dm { display: grid; grid-template-columns: 320px minmax(0,1fr); gap: 0; height: calc(100dvh - var(--player-h) - 24px); }
.omni-dm-list { border-right: 1px solid var(--line); overflow: auto; }
.omni-dm-row {
  display: flex; gap: 11px; align-items: center; padding: 12px 14px;
  border-bottom: 1px solid var(--line); cursor: pointer; text-align: left; width: 100%;
}
.omni-dm-row:hover { background: var(--card-hover); }
.omni-dm-row[aria-current="true"] { background: var(--surface-2); box-shadow: inset 2px 0 0 var(--violet); }
.omni-dm-row .m { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 2px; }
.omni-dm-row .who { font: 700 13.5px/1.3 var(--sans); display: flex; align-items: center; gap: 5px; }
.omni-dm-row .prev { font: 500 12.5px/1.35 var(--sans); color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.omni-dm-row .when { font: 500 11px/1 var(--mono); color: var(--text-3); align-self: flex-start; }
.omni-dm-row[data-unread="1"] .prev { color: var(--text-2); font-weight: 600; }
.omni-dm-row[data-unread="1"]::after { content:''; width:8px; height:8px; border-radius:50%; background: var(--teal); flex:0 0 8px; }

.omni-dm-thread { display: flex; flex-direction: column; min-width: 0; }
.omni-dm-head { display: flex; align-items: center; gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--line); }
.omni-dm-body { flex: 1; overflow: auto; padding: 16px; display: flex; flex-direction: column; gap: 8px; }
.omni-msg { max-width: 68%; padding: 9px 13px; border-radius: var(--r-lg);
  font: 500 14px/1.45 var(--sans); word-wrap: break-word; overflow-wrap: anywhere; }
.omni-msg[data-me="0"] { align-self: flex-start; background: var(--surface-2); border-bottom-left-radius: var(--r-xs); }
.omni-msg[data-me="1"] { align-self: flex-end; background: var(--violet); color: var(--brand-ink); border-bottom-right-radius: var(--r-xs); }
.omni-msg-when { font: 500 10.5px/1 var(--mono); color: var(--text-3); padding: 0 4px; }
.omni-dm-compose { display: flex; gap: 10px; align-items: flex-end; padding: 12px 16px; border-top: 1px solid var(--line); }
.omni-dm-compose textarea { flex: 1; min-height: 42px; max-height: 140px; resize: none;
  background: var(--surface-2); border: 1px solid var(--line-2); border-radius: var(--r-lg);
  color: var(--text); padding: 11px 14px; font: 500 14px var(--sans); }
/* the state that matters most: you cannot message this person */
.omni-dm-locked { padding: 28px 20px; text-align: center; color: var(--text-3); font-size: 13.5px; line-height: 1.6; }
.omni-dm-locked .lk { display: block; margin: 0 auto 10px; width: 34px; height: 34px; color: var(--text-4); }
@media (max-width: 900px) {
  .omni-dm { grid-template-columns: minmax(0,1fr); }
  .omni-dm[data-view="thread"] .omni-dm-list { display: none; }
  .omni-dm[data-view="list"] .omni-dm-thread { display: none; }
}

/* 20. NOW PLAYING: queue list, drag-to-reorder, lyrics. */
#omni-np-sheet .sheet-tabs { margin: 0 0 12px; }          /* uses .omni-tabs */
.omni-queue { display: flex; flex-direction: column; gap: 2px; padding-bottom: 12px; }
.omni-queue-head { display: flex; align-items: center; gap: 10px; padding: 4px 2px 8px; }
.omni-queue-head .eyebrow { flex: 1; }
.omni-q-row {
  display: flex; align-items: center; gap: 11px; padding: 9px 10px;
  border-radius: var(--r-md); background: transparent; width: 100%; text-align: left;
  transition: background var(--dur-2) var(--ease-out);
}
.omni-q-row:hover { background: var(--surface-2); }
.omni-q-row[data-playing="1"] { background: var(--violet-dim); }
.omni-q-row[data-playing="1"] .q-t { color: var(--violet-2); font-weight: 700; }
.omni-q-row .q-grip {
  width: 22px; height: 28px; flex: 0 0 22px; display: grid; place-items: center;
  color: var(--text-4); cursor: grab; touch-action: none;
  transition: color var(--dur-2) var(--ease-out);
}
.omni-q-row .q-grip:active { cursor: grabbing; }
.omni-q-row:hover .q-grip { color: var(--text-3); }
.omni-q-row .q-art { width: 38px; height: 38px; flex: 0 0 38px; border-radius: var(--r-xs);
  object-fit: cover; background: var(--surface-3); }
.omni-q-row .q-m { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 1px; }
.omni-q-row .q-t { font: 600 13.5px/1.3 var(--sans); color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.omni-q-row .q-s { font: 500 12px/1.3 var(--sans); color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.omni-q-row .q-dur { font: 500 11.5px/1 var(--mono); color: var(--text-3); flex: 0 0 auto; }
.omni-q-row .q-x { opacity: 0; color: var(--text-3); padding: 4px; border-radius: 50%; flex: 0 0 auto;
  transition: opacity var(--dur-2) var(--ease-out), color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out); }
.omni-q-row:hover .q-x, .omni-q-row:focus-within .q-x { opacity: 1; }
.omni-q-row .q-x:hover { color: var(--rose); background: rgba(244,63,94,.12); }

/* drag states — the row being dragged, and the gap it will land in */
.omni-q-row[data-dragging="1"] {
  opacity: .55; background: var(--surface-3); box-shadow: var(--shadow-pop);
  cursor: grabbing;
}
.omni-q-row[data-dropbefore="1"] { box-shadow: inset 0 2px 0 var(--violet); }
.omni-q-row[data-dropafter="1"]  { box-shadow: inset 0 -2px 0 var(--violet); }
.omni-queue[data-reordering="1"] .omni-q-row { transition: transform var(--dur-2) var(--ease-out); }
/* keyboard reorder affordance — grip is focusable, ↑/↓ move the row */
.omni-q-row .q-grip:focus-visible { outline: 2px solid var(--violet-2); outline-offset: 1px; border-radius: var(--r-xs); }
.omni-queue-hint { font: 500 11.5px/1.5 var(--sans); color: var(--text-3); padding: 6px 2px 0; }

/* LYRICS ---------------------------------------------------- */
.omni-lyrics {
  display: flex; flex-direction: column; gap: 14px;
  padding: 8px 4px 24px; max-height: 46vh; overflow: auto;
  scroll-behavior: smooth; overscroll-behavior: contain;
}
.omni-lyrics p {
  margin: 0; font: 600 19px/1.42 var(--sans); color: var(--text-4);
  transition: color var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-out);
  transform-origin: left center;
}
.omni-lyrics p[data-state="past"]    { color: var(--text-3); }
.omni-lyrics p[data-state="active"]  { color: var(--text); transform: scale(1.03); }
.omni-lyrics p[data-state="future"]  { color: var(--text-3); opacity: .62; }
/* unsynced lyrics: no timing data, so no highlighting — just readable prose */
.omni-lyrics[data-synced="0"] p { color: var(--text-2); font-size: 16px; font-weight: 500; transform: none; }
.omni-lyrics-empty { padding: 30px 8px; text-align: center; color: var(--text-3); font-size: 13.5px; }
@media (max-width: 700px) { .omni-lyrics p { font-size: 17px; } }

/* 21. .omni-timerange-span carries a readable date range, so it
   uses --text-3, not the decorative --text-4. */
.omni-timerange-span { color: var(--text-3); }

/* 22. Icon size utilities. */
.ico    { width:22px; height:22px; flex:0 0 22px; stroke:currentColor; fill:none;
          stroke-width:1.9; stroke-linecap:round; stroke-linejoin:round; }
.ico-14 { width:14px; height:14px; flex:0 0 14px; }
.ico-16 { width:16px; height:16px; flex:0 0 16px; }
.ico-18 { width:18px; height:18px; flex:0 0 18px; }
.ico-20 { width:20px; height:20px; flex:0 0 20px; }
.ico-24 { width:24px; height:24px; flex:0 0 24px; }
.ico-32 { width:32px; height:32px; flex:0 0 32px; }

/* 23. NARROW-VIEWPORT GUARDS
   Prevents document-level horizontal scroll at 390px. The cause is a
   flex row whose children are nowrap and non-shrinking, so the row
   forces the document wider than the viewport. Fixed at the row, not
   by hiding overflow on an ancestor. */
html { max-width: 100%; overflow-x: clip; }
body.omni-shell { max-width: 100vw; }

/* any flex/grid child that holds text must be allowed to shrink */
body.omni-shell .meta,
body.omni-shell .np-meta,
body.omni-shell .q-m,
body.omni-shell .n-body,
body.omni-shell .omni-dm-row .m { min-width: 0; }

body.omni-shell .track-title,
body.omni-shell .track-sub,
body.omni-shell .post-body,
body.omni-shell .author { overflow-wrap: anywhere; }

@media (max-width: 700px) {
  /* two-tier reflow: metadata on the first line, actions on their own */
  body.omni-shell .track-foot {
    flex-wrap: wrap; justify-content: flex-start; row-gap: 10px;
  }
  body.omni-shell .track-actions {
    flex-shrink: 1; flex-wrap: wrap; width: 100%; gap: 8px;
  }
  body.omni-shell .track-actions > * { flex: 1 1 auto; text-align: center; }
  body.omni-shell .act-right { width: 100%; justify-content: flex-start; }
}

/* 24. SAFE AREA
   Pages render under viewport-fit=cover, so the first in-flow element
   sits beneath the status bar unless it pads for the inset. */
#omniBetaBanner {
  padding-top: calc(7px + env(safe-area-inset-top)) !important;
  position: relative; z-index: var(--z-toast);
}
body.omni-shell #omni-topbar { padding-top: env(safe-area-inset-top); height: auto; min-height: var(--nav-h); }

/* 25. LEGACY CONTROL NORMALISATION
   Applies only under the shell, so unshelled pages are unaffected. */
/* a field inside a bordered card produced a double border */
body.omni-shell .card > .search,
body.omni-shell .card > input[type="search"] { border: 0; background: transparent; padding-left: 0; padding-right: 0; }
body.omni-shell input[type="search"] {
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-pill); padding: 11px 16px; min-height: 44px;
}
/* native file inputs do not match any other control on the page */
body.omni-shell input[type="file"] {
  width: 100%; padding: 10px 12px; min-height: 44px;
  background: var(--surface-2); border: 1px dashed var(--line-2);
  border-radius: var(--r-md); color: var(--text-3); font: 500 13px var(--sans);
}
body.omni-shell input[type="file"]::file-selector-button {
  margin-right: 12px; padding: 8px 14px; border: 0; border-radius: var(--r-pill);
  background: var(--surface-3); color: var(--text); font: 700 12.5px var(--sans); cursor: pointer;
}
body.omni-shell input[type="file"]:hover { border-color: var(--line-hi); }

/* 26. DENSITY
   Uniform padding on every element flattens hierarchy. Section labels
   and inline metadata carry it instead. */
body.omni-shell .page-title {
  font: 800 20px/1.2 var(--display); letter-spacing: -.01em;
  margin: 4px 2px 10px; color: var(--text);
}
body.omni-shell .section-h {
  font: 700 11px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase;
  color: var(--text-3); margin: 22px 2px 8px;
}
body.omni-shell .hint, body.omni-shell .foot { font-size: 12px; line-height: 1.5; color: var(--text-3); }

/* 27. TOUCH TARGETS — 44px minimum on anything tappable */
@media (pointer: coarse) {
  body.omni-shell .act-btn,
  body.omni-shell .chip,
  body.omni-shell .post-cta,
  body.omni-shell .track-store,
  body.omni-shell .omni-timerange button { min-height: 44px; }
  body.omni-shell .np-btn { width: 44px; height: 44px; }
  body.omni-shell .rail-me-edit { width: 44px; height: 44px; }
  /* the transport keeps its asymmetric center weight when the side
     buttons grow to the touch floor: the play disc grows with them and
     the glyph keeps holding half the disc. The disc size is capped by
     the 64px mini bar — the disc must clear the bar edges AND the 2px
     progress hairline along the bar's top edge, at rest and in every
     state. Growth-on-hover cannot satisfy that inside 64px, so on
     coarse pointers the press feedback shrinks instead: a scale-down
     can never clip. */
  body.omni-shell .np-play { width: 58px; height: 58px; }
  body.omni-shell .np-play .ico { width: 29px; height: 29px; flex: 0 0 29px; }
  body.omni-shell .np-play:hover { transform: none; }
  body.omni-shell .np-play:active { transform: scale(.94); }
}
@media (pointer: coarse) and (max-width: 767px) {
  /* inside the 64px mini bar the default ring offset would overhang the
     bar edge (58px disc + 2px offset + 2px ring each side = 66px);
     zero offset keeps the whole ring inside the bar */
  body.omni-shell .np-play:focus-visible { outline-offset: 0; }
}

/* 28. WIDE TABLES
   A table with a min-width wider than the viewport forces the document
   to scroll sideways. Confine the scroll to the table's own container
   so only the table pans and the page stays put. */
@media (max-width: 700px) {
  body.omni-shell .card:has(> table),
  body.omni-shell .wrap:has(> table),
  body.omni-shell .card:has(> * > table) {
    overflow-x: auto; -webkit-overflow-scrolling: touch;
  }
  /* fallback for engines without :has() — the table scrolls itself */
  @supports not selector(:has(*)) {
    body.omni-shell table { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  }
}

/* 29. QUOTE CARD — a post quoted inside another post.
   The card is a miniature of the post structure: head, body, media, in
   that order. Media must be the LAST child — the container clips it, so
   it inherits the bottom corners. The whole card is ONE link and its
   interior stays inert (no nested interactive elements). Name, handle
   and body are user data: build them with textContent, never markup. */
.omni-quote {
  display: block; margin-top: 10px;
  border: 1px solid var(--line-2); border-radius: var(--r-lg);
  overflow: hidden; background: transparent; color: inherit;
  transition: background var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out);
}
a.omni-quote { text-decoration: none; cursor: pointer; }
.omni-quote:hover  { background: var(--card-hover); border-color: var(--line-hi); }
.omni-quote:active { background: var(--surface-2); }
.omni-quote .oq-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px 0; min-width: 0; }
/* the name holds its ground; the handle/time line truncates first */
.omni-quote .oq-name {
  flex: 0 0 auto; max-width: 55%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 700 13px/1.3 var(--sans); color: var(--text);
}
.omni-quote .oq-sub {
  flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 500 12.5px/1.3 var(--sans); color: var(--text-3);
}
.omni-quote .oq-body {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 5; overflow: hidden;
  padding: 6px 12px 12px; font: 500 13.5px/1.5 var(--sans); color: var(--text-2);
  overflow-wrap: anywhere;
}
.omni-quote .oq-body:not(:last-child) { padding-bottom: 10px; }
.omni-quote .oq-media { display: block; width: 100%; max-height: 300px; object-fit: cover; border: 0; margin: 0; }
/* the quoted post no longer exists — quiet, non-interactive */
.omni-quote[data-gone="1"] { cursor: default; }
.omni-quote[data-gone="1"]:hover { background: transparent; border-color: var(--line-2); }
.omni-quote[data-gone="1"] .oq-body { color: var(--text-3); }
/* a quote inside a quote degrades to an inset reference — never a third card */
.omni-quote .omni-quote {
  border: 0; border-left: 2px solid var(--line-2); border-radius: 0;
  background: transparent; margin: 6px 12px 10px;
}
.omni-quote .omni-quote .oq-media { display: none; }
@media (max-width: 700px) { .omni-quote .oq-media { max-height: 220px; } }

/* 30. THREAD RAIL — one vertical hairline connects a parent to its
   replies; one divider per cluster, none between rows. The rail element
   is empty and decorative (aria-hidden, no pointer events). Row rhythm
   lives INSIDE the body so rail segments meet edge-to-edge and read as
   one continuous line. Depth never indents: grouping is the rail's job,
   so the full text measure survives on narrow viewports. */
.omni-thread { padding: 12px 0 0; }
.omni-thread + .omni-thread { border-top: 1px solid var(--line); }
.omni-thread-row { display: flex; align-items: stretch; gap: 10px; }
.omni-thread-body { min-width: 0; flex: 1; padding: 0 0 14px; overflow-wrap: anywhere; }
.omni-avcol { display: flex; flex-direction: column; align-items: center; flex: 0 0 auto; }
.omni-thread-line {
  flex: 1 1 auto; width: 2px; min-height: 10px; margin-top: 4px;
  border-radius: 1px; background: var(--line); pointer-events: none;
}
.omni-thread-row:last-child .omni-thread-line { display: none; }
/* collapsed-replies row: the rail continues into the action label */
.omni-thread-more {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 0 0 12px; text-align: left; background: transparent; border: 0;
  font: 600 13px/1 var(--sans); color: var(--violet-2); cursor: pointer;
  transition: color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
/* violet TEXT on a dark surface, so it reads from the ink token, not the
   fill token — --violet is tuned to carry white ink and measures 4.09:1 as
   text here, under the 4.5:1 body floor. --violet-2 is 7.57:1. */
.omni-thread-more:hover { color: var(--violet-2); background: var(--card-hover); }
/* matches the 32px avatar column; override when the cluster uses another size */
.omni-thread-more .omni-avcol { min-width: 32px; }
.omni-thread-more .omni-thread-line { flex: 0 0 14px; min-height: 14px; margin-top: 0; display: block; }
@media (pointer: coarse) { .omni-thread-more { min-height: 44px; } }

/* 31. DOT PAGER — position indicator for swiped media. Render at most
   7 dots; when slides are hidden past an edge, that edge's outer dot is
   data-size="xs" and its neighbour data-size="s". Shrink is transform
   only, so the footprint (84px for 7 dots) never changes and nothing
   below shifts. Dots are indicators, not controls: keep the container
   aria-hidden and announce position from an adjacent live region. */
.omni-pager {
  display: flex; align-items: center; justify-content: center;
  gap: 7px; height: 20px; margin: 8px auto 0;
  /* shrink to the dots: the element IS the fixed footprint (84px at 7
     dots), so scaling edge dots can never shift surrounding layout */
  width: max-content;
}
.omni-pager .dot {
  width: 6px; height: 6px; border-radius: 50%; background: var(--line-hi);
  /* position eases like movement; the colour swap is a plain crossfade */
  transition: transform var(--dur-2) var(--ease-out), background var(--dur-2) linear;
}
.omni-pager .dot[data-size="s"]  { transform: scale(.72); }
.omni-pager .dot[data-size="xs"] { transform: scale(.5); }
.omni-pager .dot[data-on="1"] { background: var(--violet-2); transform: none; }
/* floating over artwork: theme tokens cannot guarantee contrast there,
   so the overlay variant uses shadowed white on any art, any theme */
.omni-pager[data-overlay="1"] .dot { background: rgba(255,255,255,.6); filter: drop-shadow(0 0 2px rgba(0,0,0,.45)); }
.omni-pager[data-overlay="1"] .dot[data-on="1"] { background: #fff; }

/* 32. TEXT NAV — catalog sections as bare wrapping words (storefront
   surface). The list reflows like a paragraph; the current item differs
   only in ink and weight. No pills, underlines, backgrounds or accent
   colors — the grid owns the screen. Empty sections are not rendered. */
.omni-textnav {
  display: flex; flex-wrap: wrap; justify-content: center;
  column-gap: 22px; row-gap: 10px; padding: 14px 0 18px;
}
.omni-textnav a {
  color: var(--text-3); text-decoration: none;
  font: 600 13.5px/1.4 var(--sans); letter-spacing: .04em;
  transition: color var(--dur-2) var(--ease-out);
}
.omni-textnav a:hover { color: var(--text); }
.omni-textnav a[aria-current="page"] { color: var(--text); font-weight: 700; }
@media (pointer: coarse) {
  /* hit area grows through padding; the padding then supplies the rhythm */
  .omni-textnav { column-gap: 8px; row-gap: 0; }
  .omni-textnav a { padding: 12px 10px; display: inline-flex; align-items: center; min-height: 44px; }
}

/* 33. COMMENT ROW — avatar, full-width body, and the like control in a
   fixed right rail so the action row keeps a single axis. The timestamp
   sits inline after the name (one alignment axis, supply the middot in
   the text). Dividers are drawn by the LIST, inset to where the text
   starts, so the avatar gutter reads as one column. Replies indent
   exactly ONE level; deeper replies render at the same level. Name and
   text are user data: textContent only. The row renders what the
   comments API returns and adds no identity of its own. */
.omni-comment { display: flex; align-items: flex-start; gap: 10px; padding: 12px 2px 10px; }
.omni-comment > .avatar { flex: 0 0 auto; }
.omni-comment .oc-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.omni-comment .oc-head { display: flex; align-items: baseline; gap: 6px; min-width: 0; }
.omni-comment .oc-name {
  min-width: 0; max-width: 65%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 700 13px/1.3 var(--sans); color: var(--text);
}
.omni-comment .oc-when { flex: 0 0 auto; font: 500 11.5px/1.3 var(--mono); color: var(--text-3); }
.omni-comment .oc-txt { margin: 0; font: 500 13.5px/1.5 var(--sans); color: var(--text-2); overflow-wrap: anywhere; }
.omni-comment .oc-acts { display: flex; gap: 14px; padding-top: 2px; }
.omni-comment .oc-acts button {
  background: transparent; border: 0; padding: 0; cursor: pointer;
  font: 600 12px/1 var(--sans); color: var(--text-3);
  transition: color var(--dur-2) var(--ease-out);
}
.omni-comment .oc-acts button:hover { color: var(--text); }
.omni-comment .oc-like {
  flex: 0 0 auto; width: 34px; margin-top: 2px; padding: 0;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  background: transparent; border: 0; cursor: pointer; color: var(--text-3);
  transition: color var(--dur-2) var(--ease-out);
}
.omni-comment .oc-like:hover { color: var(--text); }
.omni-comment .oc-like[aria-pressed="true"] { color: var(--teal-2); }
.omni-comment .oc-like[aria-pressed="true"] .ico { fill: currentColor; }
.omni-comment .oc-like[data-busy="1"] { opacity: .6; pointer-events: none; }
.omni-comment .oc-cnt { font: 600 11px/1 var(--mono); }
.omni-comment .oc-cnt:empty { display: none; }
/* list-drawn divider, starting where the text column starts (32px avatar + 10px gap) */
.omni-comment + .omni-comment { position: relative; }
.omni-comment + .omni-comment::before {
  content: ''; position: absolute; top: 0; left: 42px; right: 0; height: 1px; background: var(--line);
}
.omni-comment[data-reply="1"] { padding-left: 42px; }
.omni-comment[data-reply="1"] + .omni-comment[data-reply="1"]::before { left: 84px; }
@media (pointer: coarse) {
  .omni-comment .oc-like { width: 44px; min-height: 44px; justify-content: center; }
  .omni-comment .oc-acts button { min-height: 44px; display: inline-flex; align-items: center; }
}

/* 34. REDUCED MOTION — the token layer freezes animations and
   transitions globally; scroll-behavior is a separate channel and
   needs its own opt-out. */
@media (prefers-reduced-motion: reduce) {
  .omni-lyrics { scroll-behavior: auto; }
}

/* 35. SCREEN-READER-ONLY TEXT — visually hidden, still announced.
   Pairs with aria-hidden indicators (e.g. a pager's adjacent live
   region announcing the slide position). */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0;
}

/* 36. STAT TILES — a row of headline figures. A tile is one label, one
   value, and an optional foot carrying a delta and/or a sparkline. Tiles
   hold no unit of their own; the caller supplies every string. */
.omni-stats-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px,1fr));
  gap: 12px; margin: 0 0 16px;
}
/* narrow widths pin the count instead of letting auto-fit choose it, so
   four tiles land as two-and-two and then one-per-row */
@media (max-width: 700px) { .omni-stats-row { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 420px) { .omni-stats-row { grid-template-columns: 1fr; } }

.omni-stat {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 14px; min-width: 0;
}
/* top line: the label, plus an optional period or badge pushed right.
   Baseline alignment holds the two type sizes on one line. */
.omni-stat-top { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.omni-stat-top > :last-child:not(:first-child) { margin-left: auto; flex: 0 0 auto; }
.omni-stat-label {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 600 12px/1.3 var(--sans); color: var(--text-3);
}
/* Proportional figures, not tabular: tabular gives every digit the width
   of a zero, which reads loose at display size. Tabular belongs in a
   column of numbers — table rows, axis ticks — never on a standalone
   value. */
.omni-stat-value {
  margin-top: 4px;
  font-family: var(--sans); font-weight: 600;
  font-variant-numeric: proportional-nums;
  font-size: clamp(22px, 4vw, 28px); line-height: 1.1;
  letter-spacing: -.01em; color: var(--text); overflow-wrap: anywhere;
}
/* the slot under the value: a delta, a sparkline, or both */
.omni-stat-foot { display: flex; gap: 10px; align-items: center; margin-top: 6px; }
/* when both share the row the delta holds its measure and the sparkline
   takes what is left — a squeezed delta wraps its percent onto a second
   line and stops reading as one figure */
.omni-stat-foot > .omni-delta { flex: 0 0 auto; }
/* sparkline slot. The plotted line stays in the de-emphasised ink so the
   value remains the loudest mark in the tile; the current period is drawn
   in the emphasis ink. The path itself is caller-supplied. */
.omni-stat-spark { flex: 1 1 auto; height: 28px; width: 100%; min-width: 0; }
.omni-stat-spark .spark-line,
.omni-stat-spark .spark-now {
  fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;
}
.omni-stat-spark .spark-line { stroke: var(--viz-deemph); }
.omni-stat-spark .spark-now  { stroke: var(--viz-emphasis); }
.omni-stat-spark .spark-dot  { fill: var(--viz-emphasis); }
/* the one figure a view leads with. Exactly one per view is intended — a
   second lead is two leads, and the size stops carrying rank. */
.omni-stat[data-hero] .omni-stat-value { font-size: clamp(34px, 7vw, 48px); }

/* The delta that goes in .omni-stat-foot is the component already defined
   above; there is no second one. Direction is not the same as goodness —
   a rise in a measure you want falling is a bad rise — so an optional
   data-good="false" swaps which direction carries the positive ink. With
   the attribute absent nothing changes and the direction rules above
   stand. The arrow keeps tracking direction; only the ink follows the
   reading. */
.omni-delta[data-good="false"][data-dir="up"]   .pct { color: var(--rose); }
.omni-delta[data-good="false"][data-dir="down"] .pct { color: var(--ok); }

/* 37. CHART FRAME — frame, axes, legend and mark primitives. The marks
   themselves are page-supplied SVG or positioned elements; what follows
   fixes only their size, ink and spacing. */
.omni-chart {
  --chart-h: 220px;
  /* the positioned ancestor for the readout: the plot itself becomes a
     scroll box at narrow widths, and a readout anchored inside a scroll
     box gets clipped by it */
  position: relative;
  display: block; min-width: 0; padding: 14px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md);
}
.omni-chart-head {
  display: flex; align-items: baseline; flex-wrap: wrap;
  gap: 6px 14px; margin: 0 0 12px;
}
.omni-chart-title { min-width: 0; font: 700 14px/1.3 var(--sans); color: var(--text); }
.omni-chart-legend {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px 14px; margin-left: auto; min-width: 0;
}
.omni-legend-item { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
/* the swatch carries the series identity, so the label beside it keeps
   text ink — a series colour used as type is unreadable on the surface
   and makes the reader match hues instead of reading words */
.omni-legend-swatch {
  width: 10px; height: 10px; flex: 0 0 10px;
  border-radius: var(--r-xs); background: var(--viz-emphasis);
}
/* a line series keys with a stroke, a bar or area series with a block */
.omni-legend-swatch[data-mark="line"] { width: 14px; flex-basis: 14px; height: 3px; border-radius: 2px; }
.omni-legend-item .lbl {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font: 500 12px/1.3 var(--sans); color: var(--text-2);
}
.omni-legend-item[data-deemph] .lbl { color: var(--text-3); }
.omni-legend-item[data-deemph] .omni-legend-swatch { background: var(--viz-deemph); }

/* the plot box. Height is a settable custom property, so a page sizes a
   frame without redefining the component. */
.omni-chart-plot { position: relative; height: var(--chart-h); min-width: 0; }
/* recessive horizontal hairlines behind the marks */
.omni-chart-grid { position: absolute; inset: 0; pointer-events: none; }
.omni-chart-grid > .gl { position: absolute; left: 0; right: 0; height: 1px; background: var(--viz-grid); }
/* the drawn axis, one step up from the grid */
.omni-chart-axisline { position: absolute; left: 0; right: 0; bottom: 0; height: 1px; background: var(--viz-axis); }
.omni-chart-axisline[data-axis="y"] { right: auto; top: 0; width: 1px; height: auto; }
/* Tick labels are a column of numbers, and a column is exactly where
   tabular figures are right: equal digit widths keep the values aligned
   instead of drifting from row to row. */
.omni-chart-xaxis, .omni-chart-yaxis {
  font: 500 11px/1 var(--mono); font-variant-numeric: tabular-nums; color: var(--viz-tick);
}
.omni-chart-xaxis { display: flex; justify-content: space-between; gap: 8px; padding-top: 7px; }
.omni-chart-yaxis { display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; }

/* MARKS. Default ink is the emphasised series; [data-deemph] drops a mark
   to context ink so one series leads without the rest disappearing. */
.omni-mark-bar, .omni-mark-line, .omni-mark-area, .omni-mark-dot { --mark: var(--viz-emphasis); }
.omni-mark-bar[data-deemph], .omni-mark-line[data-deemph],
.omni-mark-area[data-deemph], .omni-mark-dot[data-deemph] { --mark: var(--viz-deemph); }
/* the bar rounds at the data end only and stays square on the baseline,
   so it reads as growing out of the axis rather than floating. The 1px a
   side is the 2px of surface that separates neighbours — a gap, never a
   stroke, because a stroke adds ink that is not data. */
.omni-mark-bar {
  background: var(--mark); align-self: end; min-height: 2px;
  margin: 0 1px; border-radius: 4px 4px 0 0;
}
/* a horizontal bar rounds at its right-hand end and takes the same gap
   above and below instead */
.omni-mark-bar[data-orient="row"] { align-self: stretch; margin: 1px 0; border-radius: 0 4px 4px 0; }
/* stacked fills: interior segments are square and the same 2px of surface
   separates them; only the segment at the data end keeps the round */
.omni-mark-bar[data-seg] { border-radius: 0; margin-bottom: 2px; }
.omni-mark-bar[data-seg]:last-child { margin-bottom: 0; }
.omni-mark-bar[data-seg="end"] { border-radius: 4px 4px 0 0; }
/* a horizontal stack separates along its own axis and rounds at its own
   data end, so the two variants compose instead of fighting */
.omni-mark-bar[data-orient="row"][data-seg] { margin: 1px 2px 1px 0; border-radius: 0; }
.omni-mark-bar[data-orient="row"][data-seg]:last-child { margin-right: 0; }
.omni-mark-bar[data-orient="row"][data-seg="end"] { border-radius: 0 4px 4px 0; }
.omni-mark-line { fill: none; stroke: var(--mark); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.omni-mark-area { fill: var(--mark); opacity: .12; stroke: none; }
/* the ring is the surface showing around the dot, so points that overlap
   each other or cross a line stay countable */
.omni-mark-dot {
  width: 8px; height: 8px; min-width: 8px; min-height: 8px;
  border-radius: 50%; background: var(--mark); box-shadow: 0 0 0 2px var(--surface);
}
/* the same two marks when they are drawn as SVG */
rect.omni-mark-bar, path.omni-mark-bar { fill: var(--mark); }
circle.omni-mark-dot { fill: var(--mark); stroke: var(--surface); stroke-width: 2; }

/* HOVER LAYER, on by default. It only restates what the axis and the
   accompanying text alternative already carry, so no value is reachable
   by pointer alone; keyboard focus opens the same readout. */
.omni-chart-crosshair {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: var(--viz-axis); pointer-events: none; opacity: 0;
}
.omni-chart-plot[data-hover="1"] .omni-chart-crosshair { opacity: 1; }
/* The readout is a child of the FRAME, not of the plot. Anchoring it in
   the plot would put it inside that scroll box, where a readout near an
   edge is clipped and one near the right edge adds pannable empty space. */
.omni-chart-tooltip {
  position: absolute; z-index: 2; display: none; max-width: 240px;
  padding: 8px 10px; pointer-events: none;
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-sm); box-shadow: var(--shadow-pop);
  font: 500 12px/1.45 var(--sans); color: var(--text-2);
}
.omni-chart-tooltip[data-open="1"] { display: block; }
.omni-chart-tooltip .tt-row { display: flex; align-items: baseline; gap: 8px; }
/* the reader already knows the series and wants the number, so the number
   leads and the series name follows it */
.omni-chart-tooltip .tt-val { font: 700 13px/1.3 var(--sans); font-variant-numeric: tabular-nums; color: var(--text); }
.omni-chart-tooltip .tt-key { flex: 0 0 12px; width: 12px; height: 2px; border-radius: 1px; background: var(--viz-emphasis); align-self: center; }

.omni-chart-empty {
  display: grid; place-items: center; min-height: var(--chart-h);
  padding: 20px 16px; text-align: center;
  color: var(--text-3); font: 500 13px/1.5 var(--sans);
}

/* FACETS — two measures on different scales get two stacked frames over
   one shared horizontal scale. Never two value scales on one plot: both
   are placed by whoever drew them, so every crossing point they produce
   is an artifact of that placement and says nothing about the data. If
   the two must share one frame, index both to a common base. Do not
   answer this by adding a second value axis. */
.omni-chart-facets { display: grid; gap: 10px; }
/* Only the bottom frame prints the shared horizontal ticks. Keyed on
   "has another frame after me" rather than on being the last child, so a
   trailing caption or note cannot silently strip the ticks from every
   frame and leave the stack with no scale at all. */
.omni-chart-facets > .omni-chart:has(~ .omni-chart) .omni-chart-xaxis { display: none; }
@supports not selector(:has(*)) {
  .omni-chart-facets > .omni-chart:not(:last-child) .omni-chart-xaxis { display: none; }
}

/* narrow viewports: the frame never widens the document. A plot that
   needs more room than it has pans inside its own box. */
@media (max-width: 700px) {
  .omni-chart { padding: 12px; }
  .omni-chart-legend { margin-left: 0; }
  .omni-chart-plot {
    overflow-x: auto; overflow-y: hidden;
    overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch;
  }
}

/* 38. HEATMAP GRID — one cell per bucket, fill carrying magnitude. Every
   cell carries its own accessible label, and a text alternative sits with
   the grid using the screen-reader-only utility above, so every value is
   reachable without reading colour at all. */
.omni-heatmap {
  display: grid; grid-template-columns: repeat(var(--hm-cols, 7), minmax(11px,1fr));
  gap: 3px; min-width: 0;
  /* the grid is its own scroll box: at narrow widths the cells stop at
     the 11px floor and the columns pan here instead of widening the
     document. The 4px inset is the focus ring's width plus its offset, so
     a focused edge cell keeps its whole ring inside the scroll box. */
  overflow-x: auto; overflow-y: hidden;
  overscroll-behavior-x: contain;
  -webkit-overflow-scrolling: touch; padding: 4px;
}
/* A labelled grid gets a gutter track sized to its widest label, so a row
   label is never squeezed into a cell-width column. The cell tracks keep
   the same floor and share the rest. */
.omni-heatmap[data-labels] {
  grid-template-columns: auto repeat(var(--hm-cols, 7), minmax(11px,1fr));
}
/* the 3px gap is the surface showing through — it is what keeps two
   adjacent cells of neighbouring steps readable as two cells */
.omni-hm-cell {
  aspect-ratio: 1; min-width: 11px; border: 0; padding: 0;
  border-radius: var(--r-xs); background: var(--viz-empty);
}
/* no data is an absence, not a low value: the empty cell takes the empty
   fill plus a hairline, so it can never read as "a little" */
.omni-hm-cell[data-level="0"] { background: var(--viz-empty); border: 1px solid var(--line); }
.omni-hm-cell[data-level="1"], .omni-hm-scale .sw[data-level="1"] { background: var(--viz-seq-1); }
.omni-hm-cell[data-level="2"], .omni-hm-scale .sw[data-level="2"] { background: var(--viz-seq-2); }
.omni-hm-cell[data-level="3"], .omni-hm-scale .sw[data-level="3"] { background: var(--viz-seq-3); }
.omni-hm-cell[data-level="4"], .omni-hm-scale .sw[data-level="4"] { background: var(--viz-seq-4); }
.omni-hm-cell[data-level="5"], .omni-hm-scale .sw[data-level="5"] { background: var(--viz-seq-5); }
/* hover and focus add a ring in the surface colour and leave the fill
   alone — repainting a cell would read as its value changing */
.omni-hm-cell:hover,
.omni-hm-cell:focus-visible { box-shadow: 0 0 0 2px var(--surface); }
.omni-hm-row-label, .omni-hm-col-label { font: 500 11px/1 var(--mono); color: var(--viz-tick); }
.omni-hm-col-label { text-align: center; }
.omni-hm-row-label { align-self: center; text-align: right; padding-right: 6px; }
/* the less-to-more key: the ramp in order, with the two ends named in
   words so the direction is not left to the colour */
.omni-hm-scale { display: flex; align-items: center; gap: 4px; padding-top: 8px; flex-wrap: wrap; }
.omni-hm-scale .lbl { font: 500 11px/1 var(--sans); color: var(--text-3); }
.omni-hm-scale .sw { width: 11px; height: 11px; flex: 0 0 11px; border-radius: var(--r-xs); }
.omni-hm-scale .sw[data-level="0"] { background: var(--viz-empty); border: 1px solid var(--line); }

/* REDUCED MOTION — the token layer already clamps animation and
   transition durations everywhere. Scroll-behavior is a separate channel,
   so the two scroll boxes added above opt out on their own: a
   programmatic move inside either one lands instantly. */
@media (prefers-reduced-motion: reduce) {
  .omni-chart-plot, .omni-heatmap { scroll-behavior: auto; }
}

/* 39. RAIL FOOTER ROW + AVATAR HUE POPOVER
   The footer row pairs the profile link with a small edit trigger; the
   popover offers hue swatches. Each swatch is a transparent button around
   a real avatar disc: the disc paints itself from its own hue variable,
   and nothing below sets a background or color on it — the button owns
   only the hit area and the selection ring. */
#omni-rail .rail-me-row { margin-top: auto; display: flex; align-items: center; gap: 4px; }
#omni-rail .rail-me-row .rail-me { flex: 1; min-width: 0; margin-top: 0; }
.rail-me-edit {
  flex: 0 0 auto; width: 28px; height: 28px; /* keeps the hit target above 24px */
  display: grid; place-items: center; padding: 0;
  border: 0; background: transparent; color: var(--text-3);
  border-radius: 50%; cursor: pointer;
  transition: color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
.rail-me-edit:hover { color: var(--text); background: var(--surface-2); }
/* the icon rail is too narrow for a side-by-side footer — stack it */
@media (max-width: 1099px) {
  #omni-rail .rail-me-row { flex-direction: column; align-items: center; gap: 2px; }
}

/* Popover. A direct <body> child so it can render even where the rail
   column is hidden; fixed against the viewport, visually anchored above
   the rail footer (the offset clears the player bar plus the rail's
   bottom padding and footer row). Closed state is out of the tab order
   (visibility, not just opacity); the visibility flip on close waits for
   the fade so the exit still reads. Entrance is fade + rise — transform
   only, so nothing around it ever reflows. */
#omni-hue-pop, .omni-hue-pop {
  position: fixed; left: 10px; bottom: calc(var(--player-h) + 78px);
  z-index: var(--z-modal);
  width: max-content; max-width: calc(100vw - 24px);
  padding: 12px;
  background: var(--surface-2); border: 1px solid var(--line-2);
  border-radius: var(--r-lg); box-shadow: var(--shadow-pop);
  opacity: 0; visibility: hidden; pointer-events: none;
  transform: translateY(6px);
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out),
              visibility 0s linear var(--dur-2);
}
#omni-hue-pop[data-open="1"], .omni-hue-pop[data-open="1"] {
  opacity: 1; visibility: visible; pointer-events: auto; transform: none;
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out),
              visibility 0s;
}
.omni-hue-grid { display: grid; grid-template-columns: repeat(6, auto); gap: 8px; }
.omni-hue-sw {
  display: inline-flex; padding: 2px; margin: 0;
  border: 0; background: transparent; border-radius: 50%; cursor: pointer;
}
/* selection ring draws outside the button edge; the 2px padding keeps it
   clear of the disc inside */
.omni-hue-sw[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--violet-2); }
.omni-hue-reset {
  display: block; width: 100%; margin-top: 10px; padding: 8px 12px;
  background: transparent; border: 1px solid var(--line-2);
  border-radius: var(--r-pill); color: var(--text-2);
  font: 600 12.5px/1 var(--sans); text-align: center; cursor: pointer;
  transition: background var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out),
              color var(--dur-2) var(--ease-out);
}
.omni-hue-reset:hover { background: var(--surface-3); border-color: var(--line-hi); color: var(--text); }

/* narrow viewports: the rail is gone, so the popover docks centred above
   the player + tab bar instead */
@media (max-width: 767px) {
  #omni-hue-pop, .omni-hue-pop {
    left: 50%; right: auto;
    bottom: calc(var(--tabbar-h) + var(--player-h-sm) + env(safe-area-inset-bottom) + 10px);
    transform: translate(-50%, 6px);
  }
  #omni-hue-pop[data-open="1"], .omni-hue-pop[data-open="1"] { transform: translate(-50%, 0); }
}

/* 40. FACEPILE — an overlapping avatar cluster with a trailing count.
   Identity rule for every consumer: the cluster may render only people
   the surrounding surface has ALREADY named — it must never be the
   thing that reveals who someone is. Discs after the first tuck under
   their predecessor by ~40% of their width, and reading order is
   prominence: earlier discs stack above later ones. The separation
   ring is a box-shadow in the page background colour, so the disc's
   own paint is untouched; the trailing inset pair restates the disc's
   shading, which lives on the avatar itself — box-shadow is a single
   declaration, so adding the ring must carry the shading or erase it. */
.omni-facepile {
  display: inline-flex; align-items: center;
  flex: 0 0 auto; user-select: none; -webkit-user-select: none;
  isolation: isolate; /* the stack order below stays private to the cluster */
}
.omni-facepile > .avatar {
  position: relative;
  box-shadow: 0 0 0 2px var(--bg),
    inset 0 -2px 8px rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.18);
}
/* overlap ≈ 40% of the disc width at each size */
.omni-facepile > .avatar:not(:first-child)    { margin-left: -14px; }
.omni-facepile > .avatar-sm:not(:first-child) { margin-left: -13px; }
.omni-facepile > .avatar-xs:not(:first-child) { margin-left: -10px; }
/* earlier = on top: the first face is the most prominent (clusters cap
   at five discs before the count takes over) */
.omni-facepile > .avatar:nth-child(1) { z-index: 5; }
.omni-facepile > .avatar:nth-child(2) { z-index: 4; }
.omni-facepile > .avatar:nth-child(3) { z-index: 3; }
.omni-facepile > .avatar:nth-child(4) { z-index: 2; }
.omni-facepile > .avatar:nth-child(5) { z-index: 1; }
/* the overflow count sits clear of the discs, muted */
.omni-facepile-count {
  margin-left: 6px; flex: 0 0 auto;
  font: 600 12px/1 var(--sans); color: var(--text-3);
}

/* 41. ROW GRAMMAR — the settings/studio list row. One skeleton:
   optional leading icon, a label with an optional one-line subtitle,
   and a trailing slot holding value text, a chevron, a switch, or any
   mix. The group draws hairlines BETWEEN rows only; any outer frame
   belongs to the page. Interactive rows are real anchors or buttons;
   rows carry no state of their own beyond hover and focus. */
.omni-rows { margin: 0; padding: 0; list-style: none; }
.omni-rows > .omni-row + .omni-row { border-top: 1px solid var(--line); }

.omni-row {
  display: grid;
  grid-template-columns: auto minmax(0,1fr) auto;
  grid-template-areas: "ico label end" "ico sub end";
  align-items: center; align-content: center;
  width: 100%; min-height: 44px; padding: 12px;
  margin: 0; border: 0; background: transparent;
  color: var(--text); text-align: inherit; text-decoration: none;
}
a.omni-row, button.omni-row {
  cursor: pointer; font: inherit;
  transition: background var(--dur-2) var(--ease-out);
}
a.omni-row:hover, button.omni-row:hover { background: var(--card-hover); }

.omni-row-ico {
  grid-area: ico; width: 20px; height: 20px; margin-right: 12px;
  color: var(--text-3);
  stroke: currentColor; fill: none; stroke-width: 1.9;
  stroke-linecap: round; stroke-linejoin: round;
}
.omni-row-label {
  grid-area: label; min-width: 0;
  font: 600 14.5px/1.4 var(--sans); color: var(--text);
  overflow-wrap: anywhere;
}
.omni-row-sub {
  grid-area: sub; min-width: 0;
  font: 500 12.5px/1.4 var(--sans); color: var(--text-3);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.omni-row-end {
  grid-area: end; margin-left: 12px;
  display: inline-flex; align-items: center; gap: 8px;
}
/* current-value text: the list doubles as a status readout */
.omni-row-value { font: 500 13px/1 var(--sans); color: var(--text-2); }
/* chevron drawn from two borders — no glyph text in content. The 2px
   side margin absorbs the wider diagonal bounding box of the rotation. */
.omni-row-chev {
  width: 8px; height: 8px; flex: 0 0 auto; margin: 0 2px;
  border-top: 2px solid var(--text-3); border-right: 2px solid var(--text-3);
  transform: rotate(45deg);
}

/* toggle row: the switch rides the label line; the subtitle takes the
   full width beneath both */
.omni-row-toggle { grid-template-areas: "ico label end" "ico sub sub"; }
/* the switch: a transparent button whose track and knob are drawn by
   pseudo-elements, so the hit box can outsize the drawn control — the
   negative margins keep the 44px-tall hit box from adding row height.
   The knob paints in the text ink, which flips with the theme, so it
   holds an edge on both track states without per-theme overrides. */
.omni-row-toggle [role="switch"] {
  position: relative; flex: 0 0 auto;
  width: 48px; height: 44px; margin: -6px 0;
  padding: 0; border: 0; background: transparent; cursor: pointer;
}
.omni-row-toggle [role="switch"]::before {
  content: ''; position: absolute; left: 2px; top: 50%; margin-top: -13px;
  width: 44px; height: 26px; border-radius: var(--r-pill);
  background: var(--surface-3);
  transition: background var(--dur-2) var(--ease-out);
}
.omni-row-toggle [role="switch"]::after {
  content: ''; position: absolute; left: 5px; top: 50%; margin-top: -10px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--text); box-shadow: 0 1px 3px rgba(0,0,0,.35);
  transition: transform var(--dur-2) var(--ease-out);
}
.omni-row-toggle [role="switch"][aria-checked="true"]::before { background: var(--violet); }
.omni-row-toggle [role="switch"][aria-checked="true"]::after { transform: translateX(18px); }

/* demoted label-only tier: secondary and legal links at the foot of a
   list — no subtitle, smaller, quieter. The taller line keeps the row
   at the 44px floor from padding alone. */
.omni-row-link .omni-row-label { font: 600 13.5px/1.5 var(--sans); color: var(--text-2); }

/* enforce [hidden] so a JS-injected notification bell/chip/mark-all cannot render for signed-out users */
[hidden]{display:none !important;}

/* ============================================================
   42. SHARED EMPTY STATES — one layout, every lane.
   The DB is sparse in most places this week, so an attendee meets an empty
   state far more often than a full one. Treating it as a first-class layout
   rather than a fallback is the whole point: a tidy empty state reads as
   "nothing here yet", a bare container reads as "this is broken".
   Built by OmniShell.emptyState(); the classes are also usable directly.
   ============================================================ */
.omni-empty {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 10px; padding: 56px 24px; margin: 0 auto; max-width: 380px;
}
.omni-empty--sm { padding: 32px 20px; gap: 8px; }

/* The mark is decorative and reads from the non-text tier. --text-4 is
   exactly the right token here and is now 3.02:1, so the figure is a
   visible shape rather than a smudge — the reason the tier was raised. */
.omni-empty-figure { color: var(--text-4); line-height: 0; margin-bottom: 2px; }
.omni-empty-art { width: 64px; height: 64px; display: block; }
.omni-empty--sm .omni-empty-art { width: 48px; height: 48px; }

.omni-empty-title {
  margin: 0; font: 700 17px/1.3 var(--display); letter-spacing: -.01em; color: var(--text);
}
.omni-empty--sm .omni-empty-title { font-size: 15px; }
/* body copy, so the muted TEXT token — not --text-4, which is the non-text
   tier and must never carry a line a user has to read */
.omni-empty-body {
  margin: 0; font: 400 14px/1.55 var(--sans); color: var(--text-3); max-width: 34ch;
}
.omni-empty-actions { margin-top: 8px; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; }
/* the action is a link OR a button depending on whether it has a destination;
   both must land on the same 44px touch floor */
.omni-empty-actions > * { min-height: 44px; text-decoration: none; }

/* ============================================================
   43. SKELETONS — composed shapes over the .omni-skel shimmer.
   A skeleton beats a spinner because it reserves the real layout: the page
   does not jump when content lands. These mirror the card/row/tile grammar
   the feed, library and music grids already use, so the swap is invisible.
   ============================================================ */
.omni-skel-card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-lg); padding: 14px; margin-bottom: 12px;
  display: flex; flex-direction: column; gap: 10px;
}
.omni-skel-row  { display: flex; align-items: center; gap: 12px; padding: 10px 4px; }
.omni-skel-tile { display: flex; flex-direction: column; gap: 8px; }
.omni-skel-line { padding: 6px 0; }

.omni-skel-head { display: flex; align-items: center; gap: 10px; }
.omni-skel-meta { display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto; min-width: 0; }

/* --w is set per block from JS so one class serves every line length */
.omni-skel-l     { height: 11px; width: var(--w, 100%); border-radius: var(--r-xs); }
.omni-skel-l--sm { height: 9px; }
.omni-skel-av    { width: 36px; height: 36px; border-radius: 50%; flex: 0 0 auto; }
.omni-skel-media { height: 180px; border-radius: var(--r-md); }
.omni-skel-art   { aspect-ratio: 1; width: 100%; border-radius: var(--r-md); }

@media (max-width: 767px) {
  .omni-empty { padding: 40px 18px; }
  .omni-skel-media { height: 140px; }
}

/* ============================================================
   44. EARLY-ACCESS TEASER STRIP — signed-in, dismissible.
   Sits between the topbar and #omni-app as a direct <body> child so soft
   navigation (which empties #omni-main) cannot destroy it. Full-bleed band,
   content constrained to the app's own max width so it lines up with the
   grid underneath.
   ============================================================ */
.omni-teaser {
  border-bottom: 1px solid var(--line-2);
  background:
    linear-gradient(180deg, var(--violet-dim), transparent),
    var(--surface);
}
.omni-teaser-in {
  max-width: var(--max-w); margin: 0 auto;
  display: flex; align-items: center; gap: 12px;
  padding: 10px var(--gutter);
}
.omni-teaser-tag { flex: 0 0 auto; }
/* the notice itself is body copy a user is meant to read, so --text-2 */
.omni-teaser-msg {
  margin: 0; flex: 1 1 auto; min-width: 0;
  font: 500 13.5px/1.45 var(--sans); color: var(--text-2);
}
.omni-teaser-x {
  flex: 0 0 auto; display: grid; place-items: center;
  width: 44px; height: 44px; margin: -10px -8px -10px 0;   /* 44px hit box without adding band height */
  padding: 0; border: 0; background: transparent;
  color: var(--text-3); cursor: pointer; border-radius: var(--r-pill);
  transition: color var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
}
.omni-teaser-x:hover { color: var(--text); background: var(--card-hover); }
.omni-teaser-x .ico-14 { width: 16px; height: 16px; }

/* Tasteful entrance: the strip is injected after a session probe resolves,
   so it always arrives a beat after paint. Sliding it in stops that from
   reading as a layout glitch. The global prefers-reduced-motion block in
   omni-tokens.css collapses this to a no-op. */
.omni-teaser { animation: omni-teaser-in var(--dur-4) var(--ease-out) both; }
@keyframes omni-teaser-in {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 767px) {
  .omni-teaser-in { padding: 9px 14px; gap: 10px; }
  /* the pill is a redundant label on a narrow band — the copy carries it */
  .omni-teaser-tag { display: none; }
  .omni-teaser-msg { font-size: 13px; }
}

/* ============================================================
   24. SHARED MEDIA COMPONENTS (window.OmniMedia)
   Mobile-first: the base rules are the narrow layout, and the
   pointer/width queries below only ADD desktop affordances.
   ============================================================ */

/* Bare control reset — same failure the now-playing bar hit: an unstyled
   <button> keeps the UA button face, and a light glyph on it is invisible. */
.om-car-nav, .om-car-dot, .om-vid-btn, .om-vid-big, .om-aud-play,
.om-emb-facade, .om-pl-btn, .om-pl-play {
  -webkit-appearance: none; appearance: none;
  background: transparent; border: 0; margin: 0; padding: 0;
  font: inherit; color: inherit; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.om-ico { width: 22px; height: 22px; flex: 0 0 22px; stroke: currentColor; fill: none; }
/* Announced, never shown. */
.om-sr {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
.om-car-nav:focus-visible, .om-car-dot:focus-visible, .om-vid-btn:focus-visible,
.om-vid-big:focus-visible, .om-aud-play:focus-visible, .om-emb-facade:focus-visible,
.om-pl-btn:focus-visible, .om-pl-play:focus-visible, .om-car-strip:focus-visible,
.om-aud-wave:focus-visible, .om-vid-scrub:focus-visible {
  outline: 2px solid var(--violet-2); outline-offset: 2px; border-radius: var(--r-sm);
}

/* ---------- P1 carousel ---------- */
.om-car { position: relative; border-radius: var(--r-md); overflow: hidden; background: var(--surface-3); }
.om-car-strip {
  display: flex; overflow-x: auto; overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch;
}
.om-car-strip::-webkit-scrollbar { display: none; }
.om-car-slide { flex: 0 0 100%; margin: 0; scroll-snap-align: center; scroll-snap-stop: always; }
.om-car-img { display: block; width: 100%; max-height: 70vh; object-fit: contain; background: var(--surface-3); }
.om-car[data-single="1"] .om-car-strip { overflow: visible; }

.om-car-dots {
  position: absolute; left: 0; right: 0; bottom: 8px;
  display: flex; justify-content: center; gap: 6px; pointer-events: none;
}
.om-car-dot {
  pointer-events: auto; width: 6px; height: 6px; border-radius: 50%;
  background: rgba(255, 255, 255, .45);
  transition: background var(--dur-2) var(--ease-out), transform var(--dur-2) var(--ease-out);
}
.om-car-dot[aria-selected="true"] { background: #fff; transform: scale(1.5); }
/* A 6px dot is not a 44px touch target — grow the hit area, not the ink. */
.om-car-dot::before { content: ''; position: absolute; width: 24px; height: 24px; margin: -9px; border-radius: 50%; }

.om-car-count {
  position: absolute; top: 8px; right: 8px; padding: 2px 8px; border-radius: var(--r-pill);
  font: 600 11px/1.6 var(--mono); color: #fff; background: rgba(0, 0, 0, .58);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
}
.om-car[data-count="1"] .om-car-count, .om-car[data-single="1"] .om-car-count { display: none; }

/* Arrows are a pointer affordance: a touch visitor swipes instead. */
.om-car-nav { display: none; }
@media (hover: hover) and (pointer: fine) {
  .om-car-nav {
    display: grid; place-items: center; position: absolute; top: 50%; transform: translateY(-50%);
    width: 34px; height: 34px; border-radius: 50%; color: #fff; background: rgba(0, 0, 0, .5);
    opacity: 0; transition: opacity var(--dur-2) var(--ease-out), background var(--dur-2) var(--ease-out);
  }
  .om-car:hover .om-car-nav, .om-car-nav:focus-visible { opacity: 1; }
  .om-car-nav:hover { background: rgba(0, 0, 0, .72); }
  .om-car-nav[disabled] { opacity: 0 !important; pointer-events: none; }
  .om-car-prev { left: 8px; }
  .om-car-next { right: 8px; }
}

/* ---------- P2 video ---------- */
.om-vid { position: relative; border-radius: var(--r-md); overflow: hidden; background: #000; }
.om-vid video { display: block; width: 100%; max-height: 74vh; background: #000; }
.om-vid-big {
  position: absolute; inset: 0; margin: auto; width: 62px; height: 62px; border-radius: 50%;
  display: grid; place-items: center; color: #fff; background: rgba(0, 0, 0, .55);
  backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  transition: opacity var(--dur-2) var(--ease-out), transform var(--dur-1) var(--ease-out);
}
.om-vid-big .om-ico { width: 27px; height: 27px; flex: 0 0 27px; }
.om-vid[data-playing="1"] .om-vid-big { opacity: 0; pointer-events: none; }
.om-vid-bar {
  position: absolute; left: 0; right: 0; bottom: 0; display: flex; align-items: center; gap: 8px;
  padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
  background: linear-gradient(to top, rgba(0, 0, 0, .78), rgba(0, 0, 0, 0));
  color: #fff; opacity: 1; transition: opacity var(--dur-2) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .om-vid[data-playing="1"] .om-vid-bar { opacity: 0; }
  .om-vid:hover .om-vid-bar, .om-vid:focus-within .om-vid-bar { opacity: 1; }
}
.om-vid-btn { width: 34px; height: 34px; display: grid; place-items: center; border-radius: 50%; color: #fff; flex: 0 0 34px; }
.om-vid-btn:hover { background: rgba(255, 255, 255, .16); }
.om-vid-t { font: 500 11px/1 var(--mono); flex: 0 0 auto; min-width: 34px; text-align: center; }
.om-vid-scrub { flex: 1 1 auto; min-width: 0; accent-color: var(--violet-2); height: 20px; }

/* ---------- P3 audio row ---------- */
.om-aud {
  display: grid; grid-template-columns: auto auto minmax(0, 1fr); align-items: center; gap: 10px;
  padding: 10px; border-radius: var(--r-md); background: var(--surface-2); border: 1px solid var(--line-2);
}
.om-aud[data-current="1"] { border-color: var(--violet-2); }
.om-aud-art { width: 48px; height: 48px; border-radius: var(--r-sm); object-fit: cover; flex: 0 0 48px; background: var(--surface-3); }
.om-aud[data-variant="full"] .om-aud-art { width: 64px; height: 64px; flex: 0 0 64px; }
.om-aud-play {
  width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%; display: grid; place-items: center;
  background: var(--text); color: var(--bg);
  transition: transform var(--dur-1) var(--ease-out);
}
.om-aud-play:active { transform: scale(.94); }
.om-aud-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.om-aud-head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.om-aud-title { font: 700 13.5px/1.25 var(--sans); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.om-aud-artist { font: 500 12px/1.25 var(--sans); color: var(--text-3); text-decoration: none; flex: 0 0 auto; }
a.om-aud-artist:hover { color: var(--text-2); text-decoration: underline; }
.om-aud-wave {
  display: flex; align-items: flex-end; gap: 2px; height: 34px; cursor: pointer;
  touch-action: pan-y;                 /* horizontal drags seek, vertical still scrolls */
}
.om-aud[data-variant="mini"] .om-aud-wave { height: 24px; }
.om-aud-wave i {
  flex: 1 1 auto; min-width: 1px; border-radius: 1px; background: var(--line-hi);
  transition: background var(--dur-1) var(--ease-out);
}
.om-aud-wave i.played { background: var(--violet-2); }
.om-aud-times { display: flex; justify-content: space-between; font: 500 10.5px/1 var(--mono); color: var(--text-3); }

/* ---------- P4 embed ---------- */
.om-emb {
  position: relative; border-radius: var(--r-md); overflow: hidden;
  border: 1px solid var(--line-2); background: var(--surface-2);
}
.om-emb-facade {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
  width: 100%; min-height: 190px; aspect-ratio: 16 / 9; color: var(--text-2);
  background: var(--surface-3);
  transition: background var(--dur-2) var(--ease-out);
}
.om-emb-facade:hover { background: var(--card-hover); }
.om-emb-play {
  width: 54px; height: 54px; border-radius: 50%; display: grid; place-items: center;
  background: var(--text); color: var(--bg);
}
.om-emb-play .om-ico { width: 24px; height: 24px; flex: 0 0 24px; }
.om-emb-badge { font: 700 12px/1 var(--sans); color: var(--text); }
.om-emb-note { font: 500 11px/1.3 var(--sans); color: var(--text-3); }
.om-emb-frame { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; background: #000; }
.om-emb[data-loaded="1"] .om-emb-facade { display: none; }
.om-emb-out {
  display: flex; align-items: center; justify-content: center; gap: 6px; padding: 8px;
  font: 600 12px/1 var(--sans); color: var(--text-3); text-decoration: none;
  border-top: 1px solid var(--line-2);
}
.om-emb-out:hover { color: var(--text); background: var(--card-hover); }
.om-emb-out .om-ico { width: 14px; height: 14px; flex: 0 0 14px; }

/* ---------- playlist controls ---------- */
.om-pl { display: flex; flex-direction: column; gap: 10px; }
.om-pl-controls { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; }
.om-pl-btn {
  min-height: 34px; padding: 0 12px; border-radius: var(--r-pill); color: var(--text-2);
  font: 600 12px/1 var(--sans); display: inline-flex; align-items: center; gap: 6px;
  background: var(--surface-2); border: 1px solid var(--line-2);
}
.om-pl-btn:hover { color: var(--text); background: var(--card-hover); }
.om-pl-btn[aria-pressed="true"] { color: var(--violet-2); border-color: var(--violet-2); }
.om-pl-play {
  width: 44px; height: 44px; flex: 0 0 44px; border-radius: 50%; display: grid; place-items: center;
  background: var(--text); color: var(--bg);
}
.om-pl-queue { display: flex; flex-direction: column; }
.om-pl-row {
  display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; align-items: center; gap: 10px;
  padding: 8px 4px; border-bottom: 1px solid var(--line-2);
}
.om-pl-n { font: 500 11px/1 var(--mono); color: var(--text-3); }
.om-pl-title { font: 600 13px/1.3 var(--sans); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.om-pl-artist { font: 500 11.5px/1.3 var(--sans); color: var(--text-3); }
.om-pl-empty { padding: 14px 4px; font: 500 12.5px/1.4 var(--sans); color: var(--text-3); text-align: center; }

/* Touch targets reach 44px, and hover-only affordances stay visible. */
/* Not scoped to body.omni-shell: these components render on surfaces that do
   not mount the app shell, and a touch target must be 44px everywhere. */
@media (max-width: 767px) {
  .om-vid-btn { width: 44px; height: 44px; flex: 0 0 44px; }
  .om-aud-play { width: 44px; height: 44px; flex: 0 0 44px; }
  .om-emb-facade { min-height: 170px; }
  .om-pl-btn { min-height: 44px; padding: 0 14px; }
  .om-car-img { max-height: 60vh; }
  .om-car-nav { width: 44px; height: 44px; }
}

/* A visitor who asked for less motion gets no crossfades and no smooth scroll. */
@media (prefers-reduced-motion: reduce) {
  .om-car-strip { scroll-behavior: auto; }
  .om-car-dot, .om-car-nav, .om-vid-big, .om-vid-bar, .om-aud-wave i, .om-aud-play, .om-emb-facade {
    transition: none;
  }
}

/* ---------- P4g links block ---------- */
.om-lnk { display: flex; flex-direction: column; gap: 8px; }
.om-lnk-title { font: 700 13px/1.3 var(--sans); color: var(--text); }
.om-lnk-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.om-lnk-item { margin: 0; }
.om-lnk-a {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 48px; padding: 10px 14px; border-radius: var(--r-pill);
  background: var(--surface-2); border: 1px solid var(--line-2);
  text-decoration: none; color: var(--text);
  transition: background var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out),
              transform var(--dur-1) var(--ease-out);
}
.om-lnk-a:hover { background: var(--card-hover); border-color: var(--line-hi); }
.om-lnk-a:active { transform: scale(.99); }
.om-lnk-a:focus-visible { outline: 2px solid var(--violet-2); outline-offset: 2px; }
.om-lnk-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.om-lnk-label { font: 600 14px/1.3 var(--sans); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.om-lnk-note { font: 500 11.5px/1.3 var(--sans); color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The destination host, always visible on an off-site row — the label is
   author-supplied, so the host is what makes a mismatch legible before a tap. */
.om-lnk-side { display: flex; align-items: center; gap: 6px; flex: 0 0 auto; color: var(--text-3); }
.om-lnk-host { font: 500 11px/1 var(--mono); max-width: 42vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.om-lnk-side .om-ico { width: 13px; height: 13px; flex: 0 0 13px; }
.om-lnk-cap { font: 500 11px/1.3 var(--sans); color: var(--text-3); text-align: center; padding-top: 2px; }
@media (max-width: 767px) {
  .om-lnk-a { min-height: 52px; }
  .om-lnk-host { max-width: 34vw; }
}
@media (prefers-reduced-motion: reduce) { .om-lnk-a { transition: none; } }
.om-lnk-empty {
  padding: 16px 8px; text-align: center; border-radius: var(--r-md);
  border: 1px dashed var(--line-2); font: 500 12.5px/1.4 var(--sans); color: var(--text-3);
}

/* ============================================================
   25. COVER ART PLACEHOLDER
   A cover is missing far more often than it is broken: a legacy bare storage
   path, a track uploaded without art, a slow CDN. All three used to leave a
   hole — visibility:hidden takes the tile's background with it, and a 404 src
   paints the browser's broken-image glyph. One placeholder covers all of them,
   and the tile keeps its geometry so rows in a list stay aligned.
   The glyph is an inline constant, so it costs no request.
   ============================================================ */
.np-art[data-empty="1"], .om-aud-art[data-empty="1"], .sheet-art[data-empty="1"] {
  background-color: var(--surface-3);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23F4F1FF' stroke-opacity='.32' stroke-width='1.7' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='6.6' cy='17.6' r='2.9'/%3E%3Ccircle cx='17.6' cy='15.4' r='2.9'/%3E%3Cpath d='M9.5 17.6V6.3l11-2.1v11.2'/%3E%3Cpath d='M9.5 9.4l11-2.1'/%3E%3C/svg%3E");
  background-size: 44%;
  background-position: center;
  background-repeat: no-repeat;
}
/* the tile must never collapse while a cover is still loading */
.np-art, .om-aud-art, .sheet-art { background-color: var(--surface-3); }

/* ============================================================
   26. GATED CONTENT CARD
   Media-first weight, text-first order — the card leads with the reason in
   words and gives the lock mark the visual anchor.
   ============================================================ */
.om-gate {
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px;
  align-items: start; padding: 16px; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px dashed var(--line-hi);
}
.om-gate[data-density="compact"] { padding: 12px; gap: 10px; }
.om-gate-mark {
  width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%;
  display: grid; place-items: center; background: var(--surface-3); color: var(--text-2);
}
.om-gate[data-tone="support"] .om-gate-mark { color: var(--teal); }
.om-gate[data-tone="follow"]  .om-gate-mark { color: var(--violet-2); }
.om-gate[data-density="compact"] .om-gate-mark { width: 32px; height: 32px; flex: 0 0 32px; }
.om-gate[data-density="compact"] .om-gate-mark .om-ico { width: 17px; height: 17px; flex: 0 0 17px; }
.om-gate-body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.om-gate-badge {
  align-self: flex-start; font: 700 10px/1 var(--sans); letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-3); padding: 4px 8px; border-radius: var(--r-pill); background: var(--surface-3);
}
.om-gate-title { font: 700 15px/1.3 var(--sans); color: var(--text); }
.om-gate[data-density="compact"] .om-gate-title { font-size: 13.5px; }
.om-gate-text { margin: 0; font: 500 13px/1.45 var(--sans); color: var(--text-3); }
.om-gate-cta {
  grid-column: 2; justify-self: start; margin-top: 4px;
  display: inline-flex; align-items: center; min-height: 44px; padding: 0 18px;
  border-radius: var(--r-pill); font: 700 13px/1 var(--sans); text-decoration: none;
  background: var(--text); color: var(--bg);
  transition: transform var(--dur-1) var(--ease-out), opacity var(--dur-2) var(--ease-out);
}
.om-gate-cta:hover { opacity: .9; }
.om-gate-cta:active { transform: scale(.98); }
.om-gate-cta:focus-visible { outline: 2px solid var(--violet-2); outline-offset: 2px; }

/* ============================================================
   27. CARD DENSITY — one component, three densities
   Instagram media-first x X text-first: the words lead in ORDER, the media
   leads in WEIGHT. Lanes set data-density; they never fork the component.
     full    feed + profile posts — the reading surface
     compact explore/search rows — scanning, media forward, meta minimal
     thumb   profile media grid — the media-first projection of the same post
   ============================================================ */
[data-density="full"]    { --card-pad: 16px; --card-gap: 12px; --card-title: 15px; --card-meta: 12.5px; }
[data-density="compact"] { --card-pad: 12px; --card-gap: 10px; --card-title: 13.5px; --card-meta: 11.5px; }
[data-density="thumb"]   { --card-pad: 0;    --card-gap: 0;    --card-title: 0;     --card-meta: 0; }
/* thumb is pure media: any text slot collapses rather than being hidden, so the
   grid cell is exactly the image and nothing reserves space it will not use */
/* Scoped to the gate's OWN density, not to any ancestor that happens to carry
   the attribute — an unscoped descendant selector let a wrapper anywhere in the
   document strip the reason text out of a security notice. */
.om-gate[data-density="thumb"] .om-gate-badge,
.om-gate[data-density="thumb"] .om-gate-text { display: none; }
.om-gate[data-density="thumb"] { padding: 10px; }

/* ============================================================
   28. PLAYER POLISH — waveform + mini/full transitions
   ============================================================ */
/* Bars carry a played gradient rather than a flat fill, so position reads at a
   glance on a 24px strip. width/height animate on the density switch, but the
   PLAYED state does not fade — a seek must land instantly or it feels laggy. */
.om-aud-wave { transition: height var(--dur-2) var(--ease-out); }
.om-aud-wave i {
  border-radius: 1.5px;
  transition: background-color var(--dur-1) var(--ease-out);
}
.om-aud-wave i.played { background: var(--violet-2); }
.om-aud[data-playing="1"] .om-aud-wave i.played { background: var(--grad-brand, var(--violet-2)); }
/* hover raises the whole strip's contrast, so the scrub target is obvious
   before the pointer commits */
.om-aud-wave:hover i { background-color: var(--line-hi); }
.om-aud-wave:hover i.played { background: var(--violet-2); }
.om-aud-wave[data-dragging="1"] { cursor: grabbing; }

/* Mini -> full is one transform, not a re-layout: the row grows from its own
   top edge so the artwork does not jump between the two states. */
.om-aud { transition: padding var(--dur-2) var(--ease-out), border-color var(--dur-2) var(--ease-out); }
.om-aud-art { transition: width var(--dur-2) var(--ease-out), height var(--dur-2) var(--ease-out); }
.om-aud[data-current="1"] { box-shadow: 0 0 0 1px var(--violet-2) inset; }

@media (prefers-reduced-motion: reduce) {
  .om-aud, .om-aud-art, .om-aud-wave, .om-aud-wave i, .om-gate-cta { transition: none; }
}
