/* ==========================================================================
   Background — ember particles + gradient blur blobs
   ========================================================================== */

#ember-canvas {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

.bg-glow {
  position: fixed;
  z-index: 0;
  border-radius: 50%;
  filter: blur(90px);
  opacity: 0.35;
  pointer-events: none;
  animation: drift 22s ease-in-out infinite alternate;
}

.bg-glow--1 {
  width: 480px; height: 480px;
  background: radial-gradient(circle, var(--accent), transparent 70%);
  top: -120px; left: -100px;
}

.bg-glow--2 {
  width: 560px; height: 560px;
  background: radial-gradient(circle, var(--ember-2), transparent 70%);
  bottom: -160px; right: -140px;
  animation-delay: -8s;
}

@keyframes drift {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(40px, 30px) scale(1.08); }
}

/* ==========================================================================
   Page shell
   ========================================================================== */

.dashboard-wrap {
  position: relative;
  z-index: 1;
  max-width: 1280px;
  margin: 0 auto;
  padding: 28px 24px 64px;
}

/* ---------- Header ---------- */

.site-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  flex-wrap: wrap;
  padding-bottom: 24px;
}

.brand {
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: 'Fraunces', serif;
  font-size: 20px;
}

/* The avatar AND the wordmark are one link home, so the anchor — not
   .brand — is now the flex row that spaces them. Inherits colour so wrapping
   the text in a link doesn't restyle the wordmark. */
.brand-link {
  display: flex;
  align-items: center;
  gap: 10px;
  color: inherit;
  text-decoration: none;
}
.brand-link:hover em { color: var(--ember-2); }

.brand svg { width: 30px; height: 30px; flex-shrink: 0; }
.brand em { color: var(--accent); font-style: normal; }

.brand-avatar {
  width: 34px; height: 34px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
  border: 2px solid var(--accent);
}

.social-links {
  display: flex;
  gap: 10px;
}

.social-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 38px; height: 38px;
  border-radius: 50%;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  color: var(--ink-dim);
  transition: color var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}

.social-link:hover {
  color: var(--accent);
  border-color: var(--accent);
  transform: translateY(-2px);
}

.social-link svg { width: 16px; height: 16px; }
.social-link i { font-size: 15px; }

/* ---------- Live banner (Twitch) ---------- */

.live-banner {
  display: flex;
  align-items: center;
  gap: 10px;
  background: linear-gradient(90deg, rgba(228, 120, 90, 0.16), rgba(228, 120, 90, 0.04));
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 12px 18px;
  margin-bottom: 20px;
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  transition: filter var(--transition-fast), transform var(--transition-fast);
}

.live-banner:hover { filter: brightness(1.08); transform: translateY(-1px); }

.live-banner-dot {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--accent);
  flex-shrink: 0;
  animation: live-pulse 1.6s ease-in-out infinite;
}

@keyframes live-pulse {
  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(228, 120, 90, 0.5); }
  50% { opacity: 0.6; box-shadow: 0 0 0 6px rgba(228, 120, 90, 0); }
}

.live-banner-text { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.live-banner-viewers {
  font-size: 12px;
  font-weight: 500;
  color: var(--ink-dim);
  flex-shrink: 0;
}

/* ---------- Search bar ---------- */
/* Lives inside .site-header's own flex row now, between .brand and
   .social-links (that flex row already has justify-content:space-between,
   so a third child naturally lands in the middle) — flex-grow lets it fill
   the space between the other two on wide screens, min/max-width keeps it
   from getting crushed or absurdly wide, and .site-header's own
   flex-wrap:wrap already drops it to its own full-width line on a narrow
   window without any extra rule needed here. */

.search-bar {
  position: relative;
  flex: 1 1 220px;
  max-width: 420px;
}

.search-bar-icon {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--ink-dim);
  font-size: 13px;
  pointer-events: none;
}

.search-bar input[type="search"] {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--line);
  color: var(--ink);
  font-family: inherit;
  font-size: 14px;
  padding: 12px 16px 12px 40px;
  border-radius: var(--radius-sm);
  transition: border-color var(--transition-fast);
}

.search-bar input[type="search"]::placeholder { color: var(--ink-dim); }
.search-bar input[type="search"]:focus { outline: none; border-color: var(--accent); }

/* ---------- Category nav ---------- */

.category-nav {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 28px;
}

.cat-btn {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  color: var(--ink-dim);
  padding: 9px 18px;
  border-radius: 999px;
  cursor: pointer;
  font-size: 13.5px;
  font-family: inherit;
  transition: all var(--transition-fast);
}

.cat-btn:hover { color: var(--ink); border-color: var(--accent); }

.cat-btn.active {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
  font-weight: 600;
}

/* ---------- Layout: content hub (2/3) + right panel (1/3) ---------- */

.dashboard-layout {
  display: flex;
  align-items: flex-start;
  gap: 24px;
}

.content-hub {
  flex: 1 1 auto;
  min-width: 0;
}

.hub-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 18px;
}

.hub-head h2 { font-size: 22px; }

.hub-empty {
  color: var(--ink-dim);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  padding: 40px;
  text-align: center;
}

/* ---------- Card grid ---------- */

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 18px;
}

.card-item {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  cursor: pointer;
  transition: transform var(--transition-fast), border-color var(--transition-fast), box-shadow var(--transition-fast);
  display: flex;
  flex-direction: column;
}

.card-item:hover,
.card-item:focus-visible,
.card-item.is-active {
  transform: translateY(-4px);
  border-color: var(--accent);
  box-shadow: var(--shadow-glow);
}

.card-thumb {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--bg-elevated) center / cover no-repeat;
  display: flex;
  align-items: flex-end;
  padding: 10px;
}

.card-thumb .card-icon {
  width: 30px; height: 30px;
  border-radius: 50%;
  background: rgba(0,0,0,0.45);
  backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
  color: var(--ink);
  position: absolute;
  top: 10px; right: 10px;
}
.card-thumb .card-icon svg { width: 15px; height: 15px; }
.card-thumb .card-icon i { font-size: 14px; }

.card-badge {
  position: absolute;
  top: 10px; left: 10px;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 3px 8px;
  border-radius: 999px;
}

.card-body {
  padding: 14px 16px 16px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  flex: 1;
}

.card-meta {
  font-size: 11px;
  color: var(--ink-dim);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 6px;
}

/* Distinguishes one variant card from another sharing the same script
   (e.g. an audio cut vs. a video cut) — see resolveCardVariantLabel() in
   includes/content.php. Right-aligned opposite the platform name via
   .card-meta's own flex layout above. */
.card-variant-label {
  color: var(--accent);
  font-weight: 700;
}

.card-title {
  font-size: 15px;
  line-height: 1.35;
}

/* Bracket tags split out of the raw title by splitTitleTags() — this
   creator's own title convention leans on them heavily (genre/kink/POV
   markers), which reads better as a scannable row of small pills than as
   part of an already-long title. Capped to 2 rows (confirmed live: a
   heavily-tagged title could otherwise wrap to 4-5 rows and push the rest
   of the card's own content way down) — 36px = two 16px-tall pills
   (10px font, 2px top/bottom padding) plus the 4px gap between them;
   overflow:hidden rather than a third partial row peeking through. */
.card-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  max-height: 36px;
  overflow: hidden;
}

.card-tag {
  font-size: 10px;
  color: var(--ink-dim);
  background: rgba(255, 255, 255, 0.06);
  padding: 2px 7px;
  border-radius: 999px;
}

.card-sub {
  margin-top: auto;
  font-size: 12px;
  color: var(--ink-dim);
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.card-sub .price { color: var(--accent); font-weight: 600; }

/* ==========================================================================
   Right panel
   ========================================================================== */

.right-panel {
  flex: 0 0 0;
  width: 0;
  opacity: 0;
  transform: translateX(16px);
  overflow: hidden;
  pointer-events: none;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  transition: flex-basis var(--transition-panel), width var(--transition-panel),
              opacity var(--transition-panel), transform var(--transition-panel);
}

.right-panel.is-open {
  flex: 0 0 var(--panel-width);
  width: var(--panel-width);
  opacity: 1;
  transform: translateX(0);
  pointer-events: auto;
  position: sticky;
  top: 24px;
  max-height: calc(100vh - 48px);
  overflow-y: auto;
}

.panel-inner {
  padding: 18px;
  display: flex;
  flex-direction: column;
  gap: 18px;
  min-width: 280px;
}

.panel-close {
  align-self: flex-start;
  width: 34px; height: 34px;
  border-radius: 50%;
  background: var(--bg-elevated);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  transition: border-color var(--transition-fast), color var(--transition-fast);
}
.panel-close:hover { border-color: var(--accent); color: var(--accent); }

.panel-media {
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-elevated);
  box-shadow: var(--shadow-glow);
}

.panel-media iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 0;
  display: block;
}

/* Spotify's embed widget — not a video, so it doesn't want the 16:9 frame
   above; 352px is Spotify's own recommended height for a track/episode
   embed with cover art (see view.php's $embedIsAudioWidget). */
.panel-media-audio iframe {
  aspect-ratio: auto;
  height: 352px;
}

.panel-media-locked {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--bg-elevated);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
/* Separate element (not the parent's own `background`) so a paywalled
   post's blur filter only affects the image, never the lock icon/text
   drawn on top of it. inset is negative so the blur radius doesn't reveal
   a sharp, un-blurred fringe at the edges. */
.panel-media-locked-bg {
  position: absolute; inset: -20px;
  background: center / cover no-repeat;
  z-index: 0;
}
.panel-media-locked-bg--blurred {
  filter: blur(24px);
  transform: scale(1.1);
}
.panel-media-locked::before {
  content: '';
  position: absolute; inset: 0;
  background: rgba(0,0,0,0.55);
  z-index: 1;
}
.panel-media-locked .lock-copy {
  position: relative;
  z-index: 2;
  text-align: center;
  color: var(--ink);
  padding: 0 20px;
}
.panel-media-locked .lock-copy svg { width: 26px; height: 26px; color: var(--accent); margin-bottom: 8px; }

/* 'player' hero_style — a full 16:9 immersive canvas (same footprint as the
   iframe/locked states): full-bleed backdrop, art square + tall waveform +
   centered play button on one stage, "Listen on X" pill floating near the
   bottom. Matches the scale/composition of the user's waveform.jpg mockup
   rather than a small compact bar. */
.panel-player {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--bg-elevated);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(10px, 3vh, 22px);
  padding: 20px 0;
}
/* $heroPlayerBg in view.php: a second piece of art for this recording if
   one exists, else the same art blurred, else a plain gradient of site
   colors — inset is negative on the image case so the blur radius (when
   used) doesn't reveal a sharp, un-blurred fringe at the edges, matching
   .panel-media-locked-bg's own approach. The dark ::before overlay keeps
   the stage/pill readable regardless of how bright the backdrop itself
   happens to be. */
.panel-player-bg {
  position: absolute; inset: -10px;
  background: center / cover no-repeat;
  z-index: 0;
}
.panel-player-bg--blurred {
  filter: blur(6px);
  transform: scale(1.15);
}
.panel-player::before {
  content: '';
  position: absolute; inset: 0;
  background:
    radial-gradient(ellipse at center, rgba(10,8,16,0.15) 35%, rgba(10,8,16,0.55) 100%),
    rgba(10,8,16,0.3);
  z-index: 1;
}
.panel-player-tag {
  position: absolute; top: 14px; left: 14px; z-index: 3;
  background: rgba(10,8,16,0.55);
  border: 1px solid var(--accent);
  color: var(--accent);
  font-size: 10px; font-weight: 700; letter-spacing: 0.05em; text-transform: uppercase;
  padding: 4px 10px;
  border-radius: 999px;
}
.panel-player-stage {
  position: relative;
  z-index: 2;
  width: 100%;
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
}
/* Fills the entire stage (not just the space left of the art square) and
   sits behind it — see .panel-player-thumb's higher z-index below. */
.panel-player-waveform {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 2px;
}
.panel-player-thumb {
  position: relative;
  z-index: 2;
  margin-left: clamp(20px, 6vw, 56px);
  flex-shrink: 0;
  width: clamp(90px, 22%, 160px);
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-sm);
  background: var(--bg) center / cover no-repeat;
  box-shadow: 0 8px 24px rgba(0,0,0,0.5);
}
.panel-player-thumb-placeholder {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--accent);
}
.panel-player-thumb-placeholder svg { width: 40px; height: 40px; }

/* Video variant of the player canvas — used when a VIDEO couldn't be
   embedded at all (age-restricted on YouTube, expired Twitch VOD) and falls
   back to this hero. The audio layout is wrong for it in three ways, so
   each is undone rather than a separate hero being built:

   - no waveform (hidden in the markup): there is no audio to draw, and fake
     bars over a video describe it as something it isn't
   - the artwork goes 16:9 and centred instead of a small square pushed to
     one side, because a video's own frame IS the artwork
   - the play button sits over that frame, which is where a video's play
     control belongs */
.panel-player--video .panel-player-stage { justify-content: center; }
.panel-player--video .panel-player-thumb {
  margin-left: 0;
  width: clamp(180px, 52%, 420px);
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 34px rgba(0,0,0,0.55);
}
/* Sits on the frame rather than beside it. */
.panel-player--video .panel-player-play {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 3;
}
/* Bars: each one's own solid background color (set inline, left-to-right
   across the whole set) is what produces the color transition. The mask
   fades each bar's own top/bottom tips to transparent (a small opacity
   taper, not a color-to-black gradient) so the horizontal color shift
   across bars stays the dominant gradient instead of every bar reading as
   "color fading to black" vertically. z-index:1 keeps them below the
   ::before/::after shine lines (z-index:2) — both are pseudo-elements of
   this same element, but without explicit z-index the flex-item bars and
   the positioned pseudo-elements don't reliably stack in front/behind
   order across browsers. */
.panel-player-waveform span {
  position: relative;
  z-index: 1;
  flex: 0 0 4px;
  width: 4px;
  border-radius: 2px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000000 22%, black 78%, transparent 100%);
  mask-image: linear-gradient(to bottom, transparent 0%, #000000 22%, black 78%, transparent 100%);
}
.panel-player-waveform::before {
  content: '';
  position: absolute; left: 0; right: 0; top: 48.75%;
  height: 14px;
  z-index: 2;
  background: linear-gradient(90deg, rgb(255 255 255 / 73%), rgba(255, 255, 255), rgba(255, 255, 255), rgb(255 255 255 / 73%));
  filter: blur(10px);
  mix-blend-mode: overlay;
  pointer-events: none;
}
.panel-player-waveform::after {
  content: '';
  position: absolute; left: 0; right: 0; top: 50%;
  height: 2px;
  z-index: 2;
  background: linear-gradient(90deg, #ffffff17, rgba(255,255,255), rgba(255,255,255), #ffffff17);
  filter: blur(1px);
  mix-blend-mode: overlay;
  pointer-events: none;
}
/* Purely decorative (see view.php) — centered over the whole stage, same
   spot as the mockup's play button relative to the art + waveform. Sized
   with vw instead of a %-based clamp() so width and height resolve
   against the same axis — a %-based clamp resolves width against the
   stage's width and height against the stage's height, which are very
   different numbers on a wide 16:9 box, producing an oval instead of a
   circle. */
.panel-player-play {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: clamp(48px, 6vw, 64px);
  height: clamp(48px, 6vw, 64px);
  border-radius: 50%;
  background: rgba(255,255,255,0.92);
  color: #1C1A17;
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 4px 18px rgba(0,0,0,0.5);
  z-index: 3;
}
.panel-player-play svg { width: 64%; height: 64%;}
/* Anchored just under the play button instead of flowing to the bottom of
   the hero box — the play button is a fixed clamp(48px,6vw,64px), so half
   its height plus a small gap lands consistently just past its edge. */
.panel-player-listen {
  position: absolute;
  top: calc(50% + clamp(24px, 3vw, 32px) + 14px);
  left: 50%;
  transform: translateX(-50%);
  z-index: 2;
  white-space: nowrap;
}

/* 'image' hero_style — just the image, no icon/text overlay. */
.panel-image-full {
  display: block;
  aspect-ratio: 16 / 9;
  background: var(--bg-elevated) center / cover no-repeat;
  border-radius: var(--radius-sm);
}

.access-options-row {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.panel-product-media {
  aspect-ratio: 1 / 1;
  background: var(--bg-elevated) center / cover no-repeat;
}

.panel-title { font-size: 17px; line-height: 1.4; }
.panel-platform { font-size: 12px; color: var(--ink-dim); text-transform: uppercase; letter-spacing: 0.05em; }

.panel-source-link {
  display: inline-block;
  margin-top: 6px;
  font-size: 12.5px;
  color: var(--accent);
}
.panel-source-link:hover { text-decoration: underline; }

/* Script variant links (view.php) — a plain accent link for a same-explicit-
   level variant (different gender tag, alternate edit); a distinctly-colored,
   bordered one when it leads to something more explicit than the current
   page, so it reads as a deliberate choice rather than a normal link. */
.view-variants {
  margin-top: 14px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-start;
}

.view-variant-link {
  font-size: 13px;
  color: var(--accent);
}
.view-variant-link:hover { text-decoration: underline; }

/* Same treatment as .view-hero-social h3 ("On Social") — that rule is
   scoped to its own parent class, so this is a standalone copy rather than
   a shared selector, for the "Alternative links" heading above the merged-
   siblings list. */
.view-variants-heading {
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-dim);
  margin: 0 0 10px;
}

/* "Alternative links" — merged-sibling postings (view.php). Each is a
   distinct bordered row (same card idea as .social-mention) rather than a
   bare inline text link, since a plain wall of accent-colored link text with
   no dividers made it impossible to tell where one link ended and the next
   began once rows carried a full title instead of just a platform name. */
.alt-links {
  margin-top: 14px;
}

.alt-links-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.alt-link-row {
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  transition: border-color var(--transition-fast);
}
.alt-link-row:hover { border-color: var(--accent); }

.alt-link-platform-icon {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--bg-elevated);
  color: var(--accent);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 14px;
}

.alt-link-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.alt-link-meta {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-dim);
}
.alt-link-meta i { width: 12px; text-align: center; }

.alt-link-title {
  font-size: 13.5px;
  color: var(--ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.alt-link-external {
  flex-shrink: 0;
  margin-left: auto;
  font-size: 11px;
  color: var(--ink-dim);
}

.view-variant-gated {
  color: var(--rose);
  border: 1px solid var(--rose);
  border-radius: 999px;
  padding: 6px 14px;
}
.view-variant-gated:hover { background: var(--rose); color: var(--bg); text-decoration: none; }

.panel-external-link {
  display: inline-block;
  text-align: center;
  border: 1px solid var(--accent);
  color: var(--accent);
  border-radius: 999px;
  padding: 10px 18px;
  font-size: 13.5px;
  font-weight: 600;
  transition: all var(--transition-fast);
  background: var(--bg);
}
.panel-external-link:hover { background: var(--accent); color: var(--bg); }

/* Admin-only way in to a product's category/related boxes from a page it
   appears on. Deliberately quieter than .panel-external-link — the shop link
   is what a visit is actually for, and this sits next to it on every merch
   card an admin sees, so it can't compete for attention. */
.merch-manage-link {
  display: inline-block;
  margin-top: 8px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
  border-bottom: 1px dashed currentColor;
}
.merch-manage-link:hover { color: var(--accent); }
/* One grid cell holding a card plus its admin link, so the link doesn't
   become a cell of its own and shove the next card sideways. */
.card-grid-cell { display: flex; flex-direction: column; }

/* Patreon CTA box */

.panel-cta {
  background: linear-gradient(155deg, var(--accent-soft), transparent);
  border: 1px solid var(--accent);
  border-radius: var(--radius-sm);
  padding: 16px;
  text-align: center;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.panel-cta p { font-size: 13.5px; color: var(--ink); margin: 0; }

.btn-cta {
  display: inline-block;
  background: var(--accent);
  color: var(--bg);
  font-weight: 700;
  padding: 12px 20px;
  border-radius: 999px;
  font-size: 14px;
  transition: filter var(--transition-fast), transform var(--transition-fast);
}
.btn-cta:hover { filter: brightness(1.1); transform: translateY(-1px); }

/* Merch mini-grid */

.panel-merch h4 {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
  margin-bottom: 10px;
  font-family: 'Inter', sans-serif;
  font-weight: 600;
}

.merch-mini-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 8px;
}

.merch-mini-item {
  aspect-ratio: 1 / 1;
  border-radius: 10px;
  background: var(--bg-elevated) center / cover no-repeat;
  border: 1px solid var(--line);
  transition: border-color var(--transition-fast), transform var(--transition-fast);
}
.merch-mini-item:hover { border-color: var(--accent); transform: translateY(-2px); }

/* ==========================================================================
   view.php — content detail page (hero player + related grid)
   ========================================================================== */

.back-link {
  display: inline-block;
  color: var(--ink-dim);
  font-size: 13.5px;
  margin-bottom: 20px;
  transition: color var(--transition-fast);
}
.back-link:hover { color: var(--accent); }

.view-hero {
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  gap: 28px;
  margin-bottom: 40px;
}

.view-hero-main {
  flex: 1 1 600px;
  min-width: 0;
  max-width: 780px;
}

/* Reuses .panel-media / .panel-media-locked (same iframe + locked-state
   treatment as the merch panel used to share with content) — full-width here
   instead of a cramped sidebar, which is the whole point. */
.view-hero-media {
  width: 100%;
}

.view-title {
  font-size: 26px;
  line-height: 1.3;
  margin: 6px 0 10px;
}

.view-description {
  margin-top: 16px;
  color: var(--ink-dim);
  font-size: 14.5px;
  line-height: 1.6;
  /* Renders the text's own newlines as breaks — do not also run this through
     nl2br() in PHP, that double-applies every line break. */
  white-space: pre-line;
}

.view-description a {
  color: var(--accent);
  font-weight: 700;
}
.view-description a:hover { text-decoration: underline; }

/* "Show more"/"Show less" toggle folding the rest of a free item's own
   description (hashtags, chapter timestamps, affiliate links, etc.) — see
   splitDescriptionForCollapse() in includes/content.php. Plain <details>/
   <summary> (same native, no-JS pattern already used for the admin Stats
   box above) rather than a JS-driven collapse. */
.view-description-more { margin-top: 6px; }

.view-description-more summary {
  cursor: pointer;
  color: var(--accent);
  font-size: 13px;
  font-weight: 600;
  list-style: none;
}
.view-description-more summary::-webkit-details-marker { display: none; }
.view-description-more summary::after { content: ' \25BE'; }
.view-description-more[open] summary::after { content: ' \25B4'; }

.view-description-more:not([open]) .view-description-more-label-open { display: none; }
.view-description-more[open] .view-description-more-label-closed { display: none; }

.view-description-more-body { margin-top: 8px; }

.view-section {
  margin-bottom: 40px;
}
/* The sidebar's own "Related"/"On Social" headings — the full-page "Related"
   grid further down uses <h2> instead (styled by .hub-head h2, shared with
   the homepage), precisely so this doesn't also catch the h3s inside
   .card-title for each related card in that section. */
.view-hero-social h3 {
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-dim);
  margin-bottom: 14px;
}

/* Groups the sidebar's "Related" and "On Social" lists so they read as two
   distinct stacks rather than one run-on list — "Related" comes first (see
   view.php's own comment on $sidebarRelated) since it's tailored to this
   visitor, unlike the generic social feed below it. */
.sidebar-section:not(:last-child) { margin-bottom: 28px; }

/* Actual social posts (Twitter/Bluesky/TikTok/Instagram) next to the hero on
   wide screens — never content platforms, see the comment in view.php. */
.view-hero-social {
  flex: 1 1 260px;
  min-width: 220px;
  max-width: 340px;
}

.social-mention-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.social-mention {
  display: flex;
  align-items: center;
  gap: 10px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px 14px;
  font-size: 13.5px;
  transition: border-color var(--transition-fast);
}
.social-mention:hover { border-color: var(--accent); }
.social-mention .card-badge { position: static; }

.social-mention-thumb {
  width: 36px; height: 36px;
  flex-shrink: 0;
  border-radius: 8px;
  background: var(--bg-elevated) center / cover no-repeat;
}

.social-mention-body {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-width: 0;
}

.social-mention-body .price {
  color: var(--accent);
  font-weight: 600;
  font-size: 12px;
}

/* Featured related-merch card (view.php sidebar, "Merch" section) — reuses
   .panel-media/.panel-product-media/.panel-external-link/.panel-platform/
   .panel-title as-is (the same "here's one product, here's how to buy it"
   pieces the homepage's right-panel single-product view already defines),
   so the two surfaces never visually drift apart. This block is only the
   small vertical stack around those reused parts, plus spacing for the
   smaller-card list right underneath for any OTHER related merch item that
   didn't win the feature slot this pageview (see pickFeaturedMerchItem() in
   includes/content.php) — those reuse .social-mention-list/.social-mention
   as-is too, same as "Related"/"On Social" right below. */
.merch-feature-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 14px;
}
.merch-feature-body .price {
  color: var(--accent);
  font-weight: 600;
  font-size: 15px;
}
.merch-feature-body .panel-title { margin: 0; }

.merch-feature-others {
  margin-top: 16px;
}

/* ==========================================================================
   Shorts — vertical short-form content (YouTube Shorts, vertical Twitch
   clips; see content.is_short's own comment in migrations.php). Everything
   here is portrait-first: a 9:16 card, and a player that keeps that shape
   instead of letterboxing a vertical video into a 16:9 box.
   ========================================================================== */

.panel-media-vertical {
  max-width: 420px;
  margin: 0 auto;
}
.panel-media-vertical iframe {
  aspect-ratio: 9 / 16;
  height: auto;
}

/* Wraps onto a second row rather than scrolling sideways — a horizontal
   scroller hides whatever doesn't fit behind a gesture people often don't
   realise is there, and these lists are short enough (4 by default) that
   wrapping costs almost nothing. */
.shorts-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 14px;
}

.short-card {
  color: inherit;
  display: block;
}
.short-card-thumb {
  position: relative;
  aspect-ratio: 9 / 16;
  border-radius: var(--radius-sm);
  background: var(--bg-elevated) center / cover no-repeat;
  border: 1px solid var(--line);
  display: flex;
  align-items: flex-end;
  padding: 8px;
  transition: border-color var(--transition-fast), transform var(--transition-fast);
}
.short-card:hover .short-card-thumb {
  border-color: var(--accent);
  transform: translateY(-2px);
}
.short-card-title {
  margin-top: 8px;
  font-size: 12.5px;
  line-height: 1.35;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.short-card:hover .short-card-title { color: var(--accent); }

/* Homepage side panel. .dashboard-wrap is max-width:1280px centred, so the
   free margin this sits in only exists once the viewport is meaningfully
   wider than that — below 1700px there's nowhere to put it without eating
   into the grid, so it's hidden and .shorts-feed-card takes over inline
   instead (and above that width, those inline cards hide, so the same
   shorts are never shown twice). Sticky rather than fixed so it scrolls
   with the page and stops at the footer. */
.shorts-panel { display: none; }
/* flex, not block — this class always rides along with .card-item, which is
   itself a flex column; overriding that here would break the card's own
   layout at exactly the widths where these cards are the only way to see a
   short at all. */
.shorts-feed-card { display: flex; }

@media (min-width: 1700px) {
  .shorts-panel {
    display: block;
    /* Fixed, anchored just outside the centred 1280px container (640px to
       its own centre + a 24px gutter) — it floats alongside the feed and
       stays put while scrolling, which is what the mockup shows. */
    position: fixed;
    top: 116px;
    left: calc(50% + 664px);
    width: 288px;
    max-height: calc(100vh - 150px);
    overflow-y: auto;
  }
  .shorts-feed-card { display: none; }
}

.shorts-panel h4 {
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-dim);
  margin: 0 0 10px;
}
.shorts-panel-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
}


/* Both live on the same right edge — a product the visitor just clicked
   wins over an ambient shorts list (see RightPanel in assets/js/dashboard.js,
   which toggles this class on <body>). */
body.product-panel-open .shorts-panel { display: none; }

/* Manually-paired related content (view.php sidebar) — deliberately the
   same visual weight as the featured-merch card right above it, since an
   admin pairing two items by hand is the same kind of "this specifically
   is worth your attention" signal a featured product is. Reuses
   .panel-media/.panel-product-media/.merch-feature-body/.panel-title as-is;
   the only thing this adds is making the whole card one clickable block
   (the merch version doesn't, since its own CTA button is the link). */
.related-feature {
  display: block;
  color: inherit;
}
.related-feature .panel-media {
  transition: box-shadow var(--transition-fast);
}
.related-feature:hover .panel-media {
  box-shadow: 0 0 0 1px var(--accent), var(--shadow-glow);
}
.related-feature:hover .panel-title {
  color: var(--accent);
}
.related-feature .card-tags {
  margin-top: 2px;
}

/* Backdrop (mobile bottom-sheet only) */

.panel-backdrop {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  z-index: 40;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--transition-fast);
}
.panel-backdrop.is-visible { opacity: 1; pointer-events: auto; }

/* ==========================================================================
   Responsive
   ========================================================================== */

@media (max-width: 900px) {
  .dashboard-layout { flex-direction: column; }

  .view-hero-main, .view-hero-social { max-width: none; width: 100%; }

  .right-panel {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    top: auto;
    width: auto;
    max-height: 82vh;
    border-radius: 20px 20px 0 0;
    transform: translateY(100%);
    transition: transform var(--transition-panel), opacity var(--transition-panel);
    z-index: 50;
  }

  .right-panel.is-open {
    flex-basis: auto;
    transform: translateY(0);
  }

  .panel-backdrop { display: block; }
}

@media (max-width: 480px) {
  .card-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); }
  .dashboard-wrap { padding: 20px 16px 48px; }
}


/* A word swapped out by the site-wide masking rules, shown ONLY to an admin
   browsing the public site (see maskWordsHtml() in includes/content.php).
   Marks what's been changed without altering the layout, and the real word
   sits in the title attribute for hover. A visitor's markup contains none of
   this — no class, no tooltip, nothing revealing. */
.masked-word {
  border-bottom: 1px dashed rgba(228,120,90,0.75);
  cursor: help;
}
/* A word masked to nothing: the original is shown struck through instead, so
   a deletion is visible rather than silently absent. */
.masked-word--removed {
  border-bottom: 0;
  text-decoration: line-through;
  opacity: 0.55;
}
