/* Shatter — public global feed and per-actor profile pages.
   Twitter-inspired dark layout, single column, 600px max content width. */

:root {
    --shatter-bg: #0f1419;
    --shatter-card: #16202a;
    --shatter-card-border: #2f3a44;
    --shatter-text: #e7e9ea;
    --shatter-muted: #8b98a5;
    --shatter-link: #1d9bf0;
    --shatter-handle: #71767b;
    /* Rendered height of .shatter-header (sticky at top: 0). Used as the top
       offset for other sticky elements below it so they don't slide behind. */
    --shatter-header-height: 57px;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
button, a, input[type="range"] { touch-action: manipulation; }
html, body { overscroll-behavior-y: contain; }

html, body {
    margin: 0;
    padding: 0;
    background: var(--shatter-bg);
    color: var(--shatter-text);
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
    font-size: 15px;
    line-height: 1.4;
    min-height: 100vh;
}

/* Themed scrollbars for every Shatter page (standalone, embed, multi — all
   share .shatter-body). Thin, dark, and just visible enough against the page
   background; brighter on hover. Doesn't touch /website pages (studio-body). */
body.shatter-body {
    scrollbar-width: thin;
    scrollbar-color: var(--shatter-card-border) transparent;
}
body.shatter-body::-webkit-scrollbar { width: 10px; height: 10px; }
body.shatter-body::-webkit-scrollbar-track { background: transparent; }
body.shatter-body::-webkit-scrollbar-thumb {
    background: var(--shatter-card-border);
    border-radius: 5px;
    border: 2px solid transparent;
    background-clip: content-box;
}
body.shatter-body::-webkit-scrollbar-thumb:hover {
    background: var(--shatter-muted);
    background-clip: content-box;
    border: 2px solid transparent;
}
body.shatter-body::-webkit-scrollbar-corner { background: transparent; }

a { color: var(--shatter-link); text-decoration: none; }
a:hover { text-decoration: underline; }

.shatter-header {
    position: sticky;
    top: 0;
    z-index: 10;
    background: rgba(15, 20, 25, 0.85);
    backdrop-filter: saturate(180%) blur(12px);
    border-bottom: 1px solid var(--shatter-card-border);
    transition: transform 200ms ease;
    will-change: transform;
}
body.shatter-embed.shatter-header-hidden .shatter-header {
    transform: translateY(-100%);
}
body.shatter-embed.shatter-header-hidden {
    --shatter-header-height: 0px;
}
/* Embed mode: take the header out of document flow entirely so the feed sits
   at viewport top 0 regardless of whether the header is currently revealed or
   hidden. Without this, position: sticky reserves 57px at the top and the
   feed is pushed down even while the header is translated off-screen. */
body.shatter-embed .shatter-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
}
body.shatter-embed .shatter-main {
    padding-top: 0;
}
/* Embed mode condensed chrome — square logo only, no right-side buttons,
   tighter padding. Volume + autoplay controls stay since they're useful to
   embed viewers (the user only called out the right-side buttons + logo). */
body.shatter-embed .shatter-header-right { display: none; }
body.shatter-embed .shatter-logo-title,
body.shatter-embed .shatter-logo-text-title { display: none; }
body.shatter-embed .shatter-logo-square,
body.shatter-embed .shatter-logo-text-square { display: inline-block; }
body.shatter-embed .shatter-logo-square { height: 24px; width: 24px; }
body.shatter-embed .shatter-header-inner {
    padding: 6px 12px;
    gap: 10px;
}
/* Fullscreen toggle button — only visible in embed mode. Rendered as the
   last child of .shatter-header-inner; margin-left:auto + the embed-mode
   hide of .shatter-header-right pushes it to the far right of the strip. */
.shatter-fullscreen-btn {
    display: none;
    margin-left: auto;
    background: transparent;
    color: var(--shatter-text);
    border: 1px solid var(--shatter-card-border);
    border-radius: 6px;
    padding: 4px 6px;
    cursor: pointer;
    line-height: 0;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.shatter-fullscreen-btn:hover {
    background: rgba(255, 255, 255, 0.06);
    border-color: var(--shatter-link);
    color: var(--shatter-link);
}
body.shatter-embed .shatter-fullscreen-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
}
.shatter-header-inner {
    max-width: 600px;
    margin: 0 auto;
    padding: 14px 16px;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 16px;
}
.shatter-logo {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-size: 20px;
    font-weight: 800;
    color: var(--shatter-text);
    letter-spacing: -0.5px;
}
.shatter-logo:hover { text-decoration: none; color: var(--shatter-link); }
.shatter-logo-title,
.shatter-logo-text-title { display: inline-block; }
.shatter-logo-square,
.shatter-logo-text-square { display: none; }
.shatter-logo-title { height: 28px; width: auto; max-width: 220px; vertical-align: middle; }
.shatter-logo-square { height: 32px; width: 32px; vertical-align: middle; border-radius: 6px; }
@media (max-width: 480px) {
    .shatter-logo-title,
    .shatter-logo-text-title { display: none; }
    .shatter-logo-square,
    .shatter-logo-text-square { display: inline-block; }
}
.shatter-subhead { color: var(--shatter-muted); font-size: 13px; }
.shatter-back {
    font-size: 13px;
    color: var(--shatter-muted);
    display: inline-flex;
    align-items: center;
}
.shatter-back-icon:hover { color: var(--shatter-link); text-decoration: none; }

.shatter-main {
    max-width: 600px;
    margin: 0 auto;
    padding: 0 0 80px;
}

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

.shatter-empty {
    padding: 60px 20px;
    color: var(--shatter-muted);
    text-align: center;
}

.shatter-card {
    display: flex;
    gap: 12px;
    padding: 14px 16px;
    border-bottom: 1px solid var(--shatter-card-border);
    cursor: pointer;
}
.shatter-card:hover { background: rgba(255,255,255,0.02); }

/* New-shat marker: shats whose id > the user's previous baseline get a left-edge
   accent + a NEW pill in the corner so users can spot which posts/replies arrived
   since their last main-feed visit. Applied to the shat-{N} element itself, which
   may be the <li> wrapper, a thread-reply container, or anything with id^="shat-". */
.shatter-shat-new {
    position: relative;
    box-shadow: inset 3px 0 0 var(--shatter-link);
}
.shatter-shat-new::after {
    content: "NEW";
    position: absolute;
    top: 8px;
    right: 8px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.08em;
    color: #fff;
    background: var(--shatter-link);
    padding: 2px 6px;
    border-radius: 4px;
    pointer-events: none;
    z-index: 1;
}

.shatter-avatar {
    flex-shrink: 0;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    overflow: hidden;
    background: #2a3640;
    display: flex;
    align-items: center;
    justify-content: center;
}
.shatter-avatar img { width: 100%; height: 100%; object-fit: cover; object-position: top; display: block; }
.shatter-avatar-fallback {
    color: var(--shatter-text);
    font-weight: 700;
    font-size: 18px;
    text-transform: uppercase;
}
.shatter-avatar-fallback-large { font-size: 36px; }

.shatter-card-body { flex: 1; min-width: 0; }
.shatter-card-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    flex-wrap: wrap;
    margin-bottom: 2px;
}
.shatter-author {
    font-weight: 700;
    color: var(--shatter-text);
}
.shatter-author:hover { text-decoration: underline; }
.shatter-handle, .shatter-time { color: var(--shatter-handle); font-weight: 400; }
.shatter-reply-label {
    display: inline-block;
    color: var(--shatter-muted);
    font-size: 13px;
    margin-bottom: 4px;
}
.shatter-reply-label:hover { color: var(--shatter-link); text-decoration: underline; }
.shatter-text {
    white-space: pre-wrap;
    word-wrap: break-word;
    overflow-wrap: anywhere;
    margin: 2px 0 8px;
}

.shatter-media {
    display: block;
    border-radius: 14px;
    overflow: hidden;
    margin: 8px 0;
    border: 1px solid var(--shatter-card-border);
    background: var(--shatter-card);
}
.shatter-media:hover { text-decoration: none; }
/* Image / video media: shrink the framed wrapper to the actual displayed media width so
   portrait 9:16 content has a snug frame instead of wide dark gutters. Landscape / square
   content's intrinsic width is ≥ container width, so fit-content clamps to 100% and the
   frame fills the card like before. */
.shatter-media-image,
.shatter-media-video {
    width: fit-content;
    max-width: 100%;
    margin: 8px auto;
}
.shatter-media-image img {
    /* Aspect-preserving fit. Wrapper provides rounded corners + background via overflow:hidden. */
    display: block;
    max-width: 100%;
    width: auto;
    height: auto;
    max-height: min(720px, 80vh);
    margin: 0 auto;
}
.shatter-media-card {
    display: flex;
    gap: 12px;
    align-items: stretch;
    padding: 0;
    color: var(--shatter-text);
}
.shatter-media-thumb {
    width: 120px;
    flex-shrink: 0;
    background: #000;
    display: flex;
    align-items: center;
    justify-content: center;
}
.shatter-media-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.shatter-media-text { display: flex; flex-direction: column; padding: 10px 12px; gap: 4px; min-width: 0; }
.shatter-media-title { font-weight: 600; }
.shatter-media-sub { color: var(--shatter-muted); font-size: 13px; }
.shatter-media-link {
    display: flex;
    flex-direction: column;
    padding: 10px 12px;
    gap: 4px;
    color: var(--shatter-text);
}
.shatter-media-domain { font-weight: 600; font-size: 13px; }
.shatter-media-url { color: var(--shatter-muted); font-size: 13px; word-break: break-all; }

/* Phase 5: YouTube iframe embeds (used by show_link with youtube_url ACF, and by
   roll_media URLs that match the YouTube URL forms). */
.shatter-media-youtube { padding: 0; background: #000; }
.shatter-media-youtube-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
}
.shatter-media-youtube-frame iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}
.shatter-media-youtube-source {
    display: block;
    padding: 8px 12px;
    background: var(--shatter-card);
    color: var(--shatter-muted);
    font-size: 13px;
}
.shatter-media-youtube-source:hover { color: var(--shatter-text); text-decoration: none; }

/* Phase 15: Shmotime episode 16:9 lazy player. Renders a poster with a centered play
   button until clicked; JS then swaps the frame contents for an <iframe> loading the
   episode permalink. Mirrors .shatter-media-youtube layout for visual consistency. */
.shatter-media-episode { padding: 0; background: #000; display: block; }
.shatter-media-episode-frame {
    position: relative;
    width: 100%;
    aspect-ratio: 16 / 9;
    cursor: pointer;
}
.shatter-media-episode-thumb { position: absolute; inset: 0; }
.shatter-media-episode-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.shatter-media-episode-play {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 64px;
    height: 64px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.65);
    color: #fff;
    border: 2px solid #fff;
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    transition: transform 0.15s ease, background 0.15s ease;
}
.shatter-media-episode-play:hover {
    background: rgba(0, 0, 0, 0.9);
    transform: translate(-50%, -50%) scale(1.08);
}
.shatter-media-episode-frame iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}
.shatter-media-episode-meta {
    display: flex;
    flex-direction: column;
    padding: 8px 12px;
    background: var(--shatter-card);
    color: var(--shatter-muted);
    font-size: 13px;
    text-decoration: none;
}
.shatter-media-episode-meta:hover { background: rgba(255,255,255,0.04); text-decoration: none; }
.shatter-media-episode-title { font-weight: 600; color: var(--shatter-text); }
.shatter-media-episode-show  { color: var(--shatter-muted); font-size: 12px; }

/* Phase 24: tweet embeds (X / Twitter blockquote widget). Twitter's iframe brings its
   own dark theme + dimensions via data-theme="dark"; we just need a clean wrapper that
   matches the rounded corners of other media. The !important on margin overrides the
   default vertical spacing widgets.js applies to the rendered iframe. */
.shatter-media-tweet {
    margin-top: 12px;
    border: none;
    background: transparent;
    /* Phase 27: Twitter's widget brings its own border + radius + background, so the
       wrapper stays minimal — no border-radius/overflow that would visually nest the
       embed inside an extra container. The blockquote/iframe margin reset below stays
       so widgets.js's default vertical spacing doesn't push the embed away from the
       card body. */
}
.shatter-media-tweet blockquote.twitter-tweet,
.shatter-media-tweet iframe {
    margin: 0 !important;
}

/* .shatter-media-video wrapper sizing lives in the combined rule above
   (with .shatter-media-image) so portrait video gets a snug frame. */

/* Make-a-Video modal — image source preview + dialogue result + upload zone. */
.shatter-video-modal-source-img {
    display: block;
    max-width: 100%;
    max-height: 220px;
    margin: 8px 0 12px;
    border-radius: 8px;
    border: 1px solid var(--shatter-card-border, #333);
}
.shatter-video-modal-dialogue {
    background: rgba(255,255,255,0.04);
    border: 1px solid var(--shatter-card-border, #333);
    border-radius: 8px;
    padding: 10px 12px;
    margin: 8px 0;
    font-size: 15px;
    line-height: 1.5;
    white-space: pre-wrap;
}
.shatter-video-modal-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin: 6px 0 12px;
    flex-wrap: wrap;
}
.shatter-video-modal-voice-row {
    font-size: 12px;
    color: var(--shatter-muted, #8b98a5);
}
.shatter-video-modal-voice-row code {
    background: rgba(255,255,255,0.06);
    padding: 1px 6px;
    border-radius: 4px;
    font-family: ui-monospace, SFMono-Regular, monospace;
}
.shatter-video-modal-upload-hint {
    font-size: 13px;
    color: var(--shatter-muted, #8b98a5);
    margin: 6px 0;
}
#shatter-video-modal hr {
    border: 0;
    border-top: 1px solid var(--shatter-card-border, #333);
    margin: 16px 0;
}
.shatter-media-video video {
    /* Aspect-preserving fit. Wrapper provides rounded corners + background via overflow:hidden. */
    display: block;
    max-width: 100%;
    width: auto;
    height: auto;
    max-height: min(720px, 80vh);
    margin: 0 auto;
}

/* ----- Custom video UI (Phase 58.2): big center play, mute pin, bottom controls. */
.shatter-video-wrap {
    position: relative;
    display: block;
    line-height: 0; /* kill the inline-leftover gap below the <video> */
    cursor: pointer;
}
/* Video itself ignores pointer events so the browser's native overlay UI (right-click
   menus, hover affordances on some platforms) never shows. The wrap captures clicks
   instead; controls inside the wrap stopPropagation so they don't double-fire.
   Exception: while the <video> is in fullscreen, restore pointer events so native
   fullscreen controls (added by JS via the `controls` attribute) are interactive. */
.shatter-video-wrap video { pointer-events: none; }
.shatter-video-wrap video:fullscreen,
.shatter-video-wrap video:-webkit-full-screen { pointer-events: auto; }

/* Big center play overlay — only when paused. */
.shatter-video-bigplay {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    border: 0;
    background: transparent;
    padding: 0;
    cursor: pointer;
    line-height: 0;
    transition: opacity 0.15s ease;
    pointer-events: auto;
}
.shatter-video-wrap[data-playing="1"] .shatter-video-bigplay { display: none; }

/* Fullscreen button — top-right. Hidden by default, fades in with the bottom UI on
   .is-active. Uses the same dark-circle treatment as the play/mute buttons. */
.shatter-video-fs {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    border: 0;
    background: rgba(0,0,0,0.55);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease, background 0.15s ease;
    z-index: 2;
}
.shatter-video-fs:hover { background: rgba(0,0,0,0.85); }
.shatter-video-wrap.is-active .shatter-video-fs {
    opacity: 1;
    pointer-events: auto;
}

/* Bottom UI container. Fade is applied to individual children (and the ::before backdrop)
   so the single mute button can be exempt — kept at its bottom-right position even when
   the rest of the UI is hidden, whenever the video is muted. */
.shatter-video-ctrl {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    padding: 8px 10px 10px;
    line-height: normal;
}
.shatter-video-ctrl::before {
    content: '';
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgba(0,0,0,0.65), rgba(0,0,0,0));
    opacity: 0;
    transition: opacity 0.18s ease;
    pointer-events: none;
    z-index: 0;
}
.shatter-video-wrap.is-active .shatter-video-ctrl::before { opacity: 1; }

.shatter-video-row,
.shatter-video-timeline { position: relative; z-index: 1; }
.shatter-video-row {
    display: flex;
    align-items: center;
    gap: 8px;
    color: #fff;
}
.shatter-video-row > .shatter-video-volume { margin-left: auto; }

/* Volume control: vertical slider that pops up ABOVE the mute icon. The mute icon
   stays anchored at the row's flex-end position whether or not the slider is shown;
   absolute positioning keeps the bottom-UI row's height stable. The slider is only
   revealed when the mouse is over the volume container (mute button or slider) — not
   the whole bottom UI — and stays open while the slider is being dragged (:active). */
.shatter-video-volume {
    position: relative;
    display: flex;
    align-items: center;
}
.shatter-video-vol-slider {
    /* Universal vertical orientation. writing-mode is the modern Chrome path (124+);
       appearance: slider-vertical is the legacy Safari/Firefox/older-Chrome path;
       -moz-orient covers Firefox versions that don't honor appearance. */
    writing-mode: vertical-lr;
    direction: rtl;             /* 0 at bottom, max at top */
    -webkit-appearance: slider-vertical;
    appearance: slider-vertical;
    -moz-orient: vertical;
    width: 28px;
    height: 100px;
    cursor: pointer;
    margin: 0;
    padding: 0;                 /* keep track flush with the slider's outer edge so the thumb can reach max */
    background: rgba(0,0,0,0.65);
    border-radius: 14px;
    /* Float DIRECTLY above the mute button (no gap) so the cursor can travel from the
       button into the slider area without leaving the volume container's hover region. */
    position: absolute;
    bottom: 100%;
    right: 0;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
}
.shatter-video-volume:hover .shatter-video-vol-slider,
.shatter-video-vol-slider:active,
.shatter-video-vol-slider:focus {
    opacity: 1;
    pointer-events: auto;
}

/* Per-child fade. The mute button stays in the flex layout (same position) regardless of
   its opacity, so it never visually "jumps" between hidden and visible UI states. */
.shatter-video-play,
.shatter-video-time,
.shatter-video-timeline,
.shatter-video-mute {
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.18s ease;
}
.shatter-video-wrap.is-active .shatter-video-play,
.shatter-video-wrap.is-active .shatter-video-time,
.shatter-video-wrap.is-active .shatter-video-timeline,
.shatter-video-wrap.is-active .shatter-video-mute {
    opacity: 1;
    pointer-events: auto;
}
/* Mute is ALWAYS visible (at its fixed position) when the video is muted, even when the
   rest of the bottom UI is hidden. */
.shatter-video-wrap[data-muted="1"] .shatter-video-mute {
    opacity: 1;
    pointer-events: auto;
}
.shatter-video-play,
.shatter-video-mute {
    width: 28px;
    height: 28px;
    border: 0;
    border-radius: 50%;
    background: rgba(0,0,0,0.55);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    transition: background 0.15s ease;
}
.shatter-video-play:hover,
.shatter-video-mute:hover { background: rgba(0,0,0,0.85); }
.shatter-video-time {
    font-size: 12px;
    font-variant-numeric: tabular-nums;
    color: #fff;
    text-shadow: 0 1px 2px rgba(0,0,0,0.55);
}

/* Seek slider — flat track + small circular thumb, dark-theme-friendly. */
.shatter-video-timeline {
    padding: 6px 4px 0;
}
.shatter-video-seek {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 14px;
    background: transparent;
    cursor: pointer;
    margin: 0;
}
.shatter-video-seek::-webkit-slider-runnable-track {
    height: 3px;
    border-radius: 2px;
    background: rgba(255,255,255,0.35);
}
.shatter-video-seek::-moz-range-track {
    height: 3px;
    border-radius: 2px;
    background: rgba(255,255,255,0.35);
}
.shatter-video-seek::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #fff;
    margin-top: -4.5px;
    border: 0;
    box-shadow: 0 0 0 1px rgba(0,0,0,0.35);
}
.shatter-video-seek::-moz-range-thumb {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    background: #fff;
    border: 0;
    box-shadow: 0 0 0 1px rgba(0,0,0,0.35);
}

.shatter-card-foot {
    margin-top: 6px;
    display: flex;
    align-items: center;
    /* Wrap to a second row instead of overflowing horizontally on narrow viewports.
       Without this, admin viewers (5+ extra chrome buttons) overflow the card on
       iPhone widths, which leaks past the body and triggers iOS Safari's whole-page
       auto-zoom-out. */
    flex-wrap: wrap;
    row-gap: 4px;
}
.shatter-replies {
    color: var(--shatter-muted);
    font-size: 13px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.shatter-replies-empty { color: var(--shatter-handle); }
.shatter-stat {
    color: var(--shatter-muted);
    font-size: 13px;
    margin-left: 12px;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}
.shatter-stat svg,
.shatter-replies svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
}

/* Phase 9.3: in-page lightbox for inline images. Backdrop dims the page, the image
   is centered with a max constraint so very tall/wide images stay readable. */
.shatter-lightbox {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
}
.shatter-lightbox[hidden] { display: none; }
.shatter-lightbox-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.85);
    cursor: zoom-out;
}
.shatter-lightbox-img {
    position: relative;
    max-width: min(95vw, 1600px);
    max-height: 90vh;
    object-fit: contain;
    cursor: zoom-out;
    box-shadow: 0 8px 60px rgba(0, 0, 0, 0.6);
    border-radius: 4px;
}
.shatter-lightbox-close {
    position: absolute;
    top: 16px;
    right: 16px;
    width: 36px;
    height: 36px;
    background: rgba(0, 0, 0, 0.6);
    color: #fff;
    border: 0;
    border-radius: 50%;
    font-size: 24px;
    line-height: 1;
    cursor: pointer;
    z-index: 1;
    font-family: inherit;
}
.shatter-lightbox-close:hover { background: rgba(0, 0, 0, 0.85); }
.shatter-no-scroll { overflow: hidden; }

/* Phase 7: Play Audio button — three states (idle / loading / playing).
   Single DOM element; state classes swap the visible glyph via ::before on the icon span. */
.shatter-play-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    margin-right: 12px;
    padding: 0;
    background: transparent;
    color: var(--shatter-muted);
    border: 1px solid var(--shatter-card-border);
    border-radius: 50%;
    cursor: pointer;
    font-family: inherit;
    font-size: 11px;
    line-height: 1;
    transition: color 0.15s, background 0.15s, border-color 0.15s;
    flex-shrink: 0;
}
.shatter-play-btn:hover { color: var(--shatter-link); background: rgba(29,155,240,0.08); }
.shatter-play-btn .shatter-play-icon { display: block; line-height: 1; }
/* Loading: hide ▶ glyph and show a spinning ring via ::before on the icon. */
.shatter-play-btn.is-loading { cursor: wait; }
.shatter-play-btn.is-loading .shatter-play-icon { font-size: 0; }
.shatter-play-btn.is-loading .shatter-play-icon::before {
    content: '';
    display: block;
    width: 12px; height: 12px;
    border: 2px solid var(--shatter-card-border);
    border-top-color: var(--shatter-link);
    border-radius: 50%;
    animation: shatter-spin 0.7s linear infinite;
}
@keyframes shatter-spin { to { transform: rotate(360deg); } }
/* Playing: hide ▶ glyph and show a ■ stop glyph via ::before; tint to active link colour. */
.shatter-play-btn.is-playing {
    color: var(--shatter-link);
    border-color: var(--shatter-link);
    background: rgba(29,155,240,0.12);
}
.shatter-play-btn.is-playing .shatter-play-icon { font-size: 0; }
.shatter-play-btn.is-playing .shatter-play-icon::before {
    content: '■';
    display: block;
    font-size: 11px;
    color: inherit;
}

.shatter-loadmore {
    text-align: center;
    padding: 20px;
}
.shatter-loadmore button {
    background: transparent;
    color: var(--shatter-link);
    border: 1px solid var(--shatter-card-border);
    padding: 8px 18px;
    border-radius: 999px;
    font-size: 14px;
    cursor: pointer;
}
.shatter-loadmore button:hover { background: rgba(29, 155, 240, 0.1); }
.shatter-loadmore button:disabled { color: var(--shatter-handle); cursor: default; }

/* Phase 4: profile banner — Twitter-style header image at the top of /shatter/{actor}.
   Renders even when no banner is set (gradient placeholder) so the layout stays stable. */
.shatter-banner {
    width: 100%;
    aspect-ratio: 3 / 1;
    background: linear-gradient(135deg, #1d3a5c, #2a3640);
    overflow: hidden;
}
.shatter-banner img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Profile page */
.shatter-profile {
    display: flex;
    gap: 16px;
    padding: 20px 16px;
    border-bottom: 1px solid var(--shatter-card-border);
}
.shatter-profile-with-banner { padding-top: 12px; }
.shatter-profile-with-banner .shatter-profile-avatar {
    margin-top: -56px;
    border: 4px solid var(--shatter-bg);
    background-clip: padding-box;
}

/* Phase 6: profile admin Generate-Shat button (only visible to logged-in admins). */
.shatter-profile-admin-actions { margin-top: 12px; }
.shatter-admin-post-btn {
    background: var(--shatter-link);
    color: #fff;
    border: 0;
    padding: 8px 16px;
    border-radius: 999px;
    font-weight: 600;
    cursor: pointer;
    font: inherit;
}
.shatter-admin-post-btn:hover { filter: brightness(1.1); }

/* Phase 14: global-feed header "Generate Shat" admin button. Sits at the right end
   of .shatter-header-inner via margin-left:auto, matches the pill button style of
   .shatter-admin-post-btn but with slightly tighter padding for the header context. */
.shatter-admin-generate-btn {
    width: 32px;
    height: 32px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--shatter-link);
    color: #fff;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.shatter-admin-generate-btn:hover { filter: brightness(1.1); }

/* Phase 6: Posts/Replies tabs above the actor's timeline. */
.shatter-tabs {
    display: flex;
    border-bottom: 1px solid var(--shatter-card-border);
}
.shatter-tab {
    flex: 1;
    text-align: center;
    padding: 14px 0;
    color: var(--shatter-muted);
    font-weight: 600;
    border-bottom: 4px solid transparent;
}
.shatter-tab:hover { background: rgba(255,255,255,0.03); text-decoration: none; }
.shatter-tab-active { color: var(--shatter-text); border-bottom-color: var(--shatter-link); }

/* Phase 34: main-page tabs hold ~7 entries (For You + each grokResearch preset),
   so flex:1 per tab would squish them. Auto-width + horizontal scroll on narrow
   viewports. */
.shatter-main-tabs { overflow-x: auto; flex-wrap: nowrap; }
.shatter-main-tabs .shatter-tab { flex: 0 0 auto; padding: 14px 16px; white-space: nowrap; }

/* Phase 56: hover-gated horizontal scrollbar — invisible by default, fades in when
   the user mouses over the strip so desktop matches the chrome-free mobile feel.
   Firefox uses scrollbar-width/scrollbar-color; WebKit uses ::-webkit-scrollbar-*.
   (WebKit doesn't animate scrollbar pseudo-element transitions — instant swap is fine.) */
.shatter-main-tabs {
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;
    transition: scrollbar-color 0.2s ease;
}
.shatter-main-tabs:hover {
    scrollbar-color: var(--shatter-card-border) transparent;
}
.shatter-main-tabs::-webkit-scrollbar        { height: 6px; }
.shatter-main-tabs::-webkit-scrollbar-track  { background: transparent; }
.shatter-main-tabs::-webkit-scrollbar-thumb  {
    background: transparent;
    border-radius: 3px;
    transition: background 0.2s ease;
}
.shatter-main-tabs:hover::-webkit-scrollbar-thumb       { background: var(--shatter-card-border); }
.shatter-main-tabs::-webkit-scrollbar-thumb:hover       { background: var(--shatter-muted); }
/* Hide WebKit's OS-default step-arrow buttons at each end of the scrollbar so desktop
   matches mobile's arrow-free look. Firefox's scrollbar-width:thin already omits these. */
.shatter-main-tabs::-webkit-scrollbar-button           { display: none; }

/* Phase 56: drag-to-scroll affordance. Hover-capable + fine-pointer devices only —
   touch users already get native swipe scrolling, and a grab cursor on touch looks
   wrong. While dragging, force the grabbing cursor on descendants too and suppress
   text selection so the drag feels native. */
@media (hover: hover) and (pointer: fine) {
    .shatter-main-tabs { cursor: grab; }
    .shatter-tabs-dragging,
    .shatter-tabs-dragging * { cursor: grabbing; }
    .shatter-tabs-dragging { user-select: none; }
}

/* Phase 34: preset-tab feed of research topics — cards mirror .shatter-card rhythm. */
.shatter-research-feed { list-style: none; margin: 0; padding: 0; }
.shatter-research-item {
    display: block;
    padding: 14px 16px;
    border-bottom: 1px solid var(--shatter-card-border);
}
.shatter-research-item-title { margin: 0 0 6px 0; font-size: 16px; line-height: 1.35; color: var(--shatter-text); }
.shatter-research-item-desc  { margin: 0 0 8px 0; color: var(--shatter-muted); line-height: 1.45; }
.shatter-research-empty {
    padding: 48px 16px;
    text-align: center;
    color: var(--shatter-muted);
    font-style: italic;
}

/* Phase 41: admin "Quote as actor…" button on each research item + modal label. */
.shatter-research-item-actions { margin-top: 10px; text-align: right; }
.shatter-admin-quote-btn {
    background: rgba(29,161,242,0.10);
    border: 1px solid var(--shatter-card-border, #333);
    color: var(--shatter-link, #1da1f2);
    padding: 4px 10px;
    border-radius: 14px;
    font-size: 12px;
    cursor: pointer;
    font-family: inherit;
}
.shatter-admin-quote-btn:hover { background: rgba(29,161,242,0.20); }
.shatter-modal-quote-label {
    font-size: 13px;
    color: var(--shatter-muted, #888);
    margin: 0 0 8px 0;
}
.shatter-modal-quote-label #shatter-modal-quote-title { color: var(--shatter-text); font-weight: 600; }

/* Phase 6: Twitter-style thread pair (parent shat + actor's reply, visually grouped
   by an avatar connector line that spans both cards). The line lives in the empty
   gutter at x=38px (= card padding-left:16 + half of avatar:44), drawn as two
   pseudo-element segments that meet at the parent/reply boundary so the visual
   appears continuous from below the parent's avatar to just above the reply's avatar. */
.shatter-thread-pair {
    list-style: none;
    border-bottom: 1px solid var(--shatter-card-border);
}
.shatter-thread-pair .shatter-card { border-bottom: 0; }
.shatter-thread-parent { position: relative; }
.shatter-thread-parent .shatter-card { padding-bottom: 4px; }
.shatter-thread-parent::after {
    content: '';
    position: absolute;
    left: 38px;
    top: 62px;       /* card padding-top (14) + avatar height (44) + small gap (4) */
    bottom: 0;       /* extends to the bottom edge of the parent container */
    width: 2px;
    background: var(--shatter-card-border);
    transform: translateX(-50%);
    pointer-events: none;
}
.shatter-thread-reply { position: relative; }
.shatter-thread-reply .shatter-card { padding-top: 4px; }
.shatter-thread-reply::before {
    content: '';
    position: absolute;
    left: 38px;
    top: 0;          /* meets the parent's segment at the boundary */
    height: 4px;     /* down to just above the reply's avatar (= reply card padding-top) */
    width: 2px;
    background: var(--shatter-card-border);
    transform: translateX(-50%);
    pointer-events: none;
}
/* Phase 56: middle replies (any .shatter-thread-reply with another reply after it
   inside the same .shatter-thread-pair group) extend the connector below their avatar
   down to the bottom of the reply container — meeting the next reply's ::before stub.
   Last reply has no ::after so the chain visually terminates at its avatar. */
.shatter-thread-reply:not(:last-child)::after {
    content: '';
    position: absolute;
    left: 38px;
    top: 62px;       /* matches .shatter-thread-parent::after — below the avatar */
    bottom: 0;
    width: 2px;
    background: var(--shatter-card-border);
    transform: translateX(-50%);
    pointer-events: none;
}
.shatter-thread-parent-missing {
    list-style: none;
    padding: 14px 16px;
    color: var(--shatter-muted);
    font-style: italic;
    border-bottom: 1px dashed var(--shatter-card-border);
    margin: 0;
}

/* Phase 13 / 13.1: nested thread tree with per-child connector segments. Each direct
   child draws its own line that meets at the avatar center, so the spine cleanly stops
   at the LAST child's avatar (no dangling) and visibly branches to each child via a
   small horizontal stub at avatar height. Replaces the Phase 11.2 flat-list connector. */
.shatter-thread-tree { list-style: none; padding: 0; margin: 0; }
.shatter-thread-node { list-style: none; }
/* Phase 13.4: every card in the tree gets position: relative (so absolute children
   like the parent connector ::after position from the card, not the viewport) and
   drops its border-bottom (indent + connector spine already separate the cards;
   horizontal dividers staircase awkwardly with nesting). Scoped to .shatter-thread-tree
   so flat global feed and actor-profile cards keep their dividers. */
.shatter-thread-tree .shatter-card { position: relative; border-bottom: none; }
.shatter-thread-replies {
    list-style: none;
    margin: 0;
    padding-left: 32px;
    position: relative;
}

/* Top segment: every direct child draws a line from its top edge down to its avatar's
   center, meeting the previous sibling's bottom segment at the boundary. Phase 13.3:
   spine moved from indent gutter (left:-26) into the parent's avatar center column
   (left:6 of node = x=38 of parent node) so it aligns with the parent connector. */
.shatter-thread-replies > .shatter-thread-node { position: relative; }
.shatter-thread-replies > .shatter-thread-node::before {
    content: '';
    position: absolute;
    left: 6px;       /* x=38 of parent node = avatar center column */
    top: 0;
    height: 36px;    /* card padding-top (14) + avatar radius (22) = avatar center */
    width: 2px;
    background: var(--shatter-card-border);
    transform: translateX(-50%);
    pointer-events: none;
}
/* Bottom segment: only NON-LAST children continue the line past the avatar through
   their own subtree to meet the next sibling's top segment. The LAST child gets no
   ::after, so the spine cleanly terminates at the last avatar. */
.shatter-thread-replies > .shatter-thread-node:not(:last-child)::after {
    content: '';
    position: absolute;
    left: 6px;
    top: 36px;
    bottom: 0;
    width: 2px;
    background: var(--shatter-card-border);
    transform: translateX(-50%);
    pointer-events: none;
}

/* Horizontal stub: short connector from the spine over to each child's avatar at
   avatar height — visually anchors each child to the spine. Spine at x=6 of node,
   avatar's left edge at x=16 of node, so stub is 10px wide. (position: relative on
   the card is provided by the broader .shatter-thread-tree .shatter-card rule above.) */
.shatter-thread-replies > .shatter-thread-node > .shatter-card::before {
    content: '';
    position: absolute;
    left: 6px;       /* aligned with the spine column */
    top: 36px;       /* avatar center */
    width: 10px;     /* spans from spine to avatar's left edge */
    height: 2px;
    background: var(--shatter-card-border);
    pointer-events: none;
}

/* Phase 13.3: PARENT connector. Any node with children draws a vertical line from
   below its avatar down to the bottom of the card, where the children's spine begins
   in the same column. Without this the root card would float orphaned with no visual
   link to its children's spine. Same column (x=38 of card) means the parent connector
   and the children spine form ONE continuous line.
   :has() selector — Chrome 105+, Safari 15.4+, Firefox 121+ (universal modern). */
.shatter-thread-node:has(> .shatter-thread-replies) > .shatter-card::after {
    content: '';
    position: absolute;
    left: 38px;      /* avatar center column of THIS card */
    top: 62px;       /* card padding-top (14) + avatar (44) + 4px gap */
    bottom: 0;       /* down to the card's bottom border */
    width: 2px;
    background: var(--shatter-card-border);
    transform: translateX(-50%);
    pointer-events: none;
}

/* Cap visual indent at level 6 — past that, stop adding padding (DOM still nests). */
.shatter-thread-replies .shatter-thread-replies .shatter-thread-replies
.shatter-thread-replies .shatter-thread-replies .shatter-thread-replies {
    padding-left: 0;
}

/* Mobile: keep the same 32px indent as desktop so the spine column has clearance
   from the child cards and the parent connector aligns. Slight horizontal cost on
   small screens vs the prior 16px indent is acceptable for visual consistency. */

/* Phase 13: focused-card flash. Triggered by the URL fragment (#shat-N) on load and by
   click on "Replying to {name}" labels when the parent is in-page. Twitter does this
   when you land on a tweet via permalink. */
.shatter-card-focused {
    animation: shatter-flash 2.5s ease-out;
}
@keyframes shatter-flash {
    0%   { background-color: rgba(255, 213, 79, 0.30); }
    100% { background-color: transparent; }
}

/* Phase 17: karaoke text reveal — character spans turn blue as audio plays them. */
.shatter-char {
    color: inherit;
    transition: color 0.12s ease-out;
}
.shatter-char.is-spoken { color: var(--shatter-link); }

/* Phase 17: speaking card — subtle background tint plus a pulsing ring on the
   speaker's avatar so you can scan the feed and see which shat is currently being
   voiced. Both clear the moment audio ends or the user clicks Stop. */
.shatter-card-speaking { background: rgba(29, 155, 240, 0.05); }
.shatter-card-speaking .shatter-avatar { position: relative; }
/* Phase 45: speaking avatar wobble — grows large, wobbles, shrinks, repeats. Animates
   the parent (not the inner img) because .shatter-avatar has overflow:hidden for the
   round clip; scaling the img alone would clip at the 44×44 boundary. The ::after ring
   below inherits this transform, which is fine — the ring is a perfect circle so the
   rotation is invisible on it, and the scale just makes the ring "breathe" along. */
.shatter-card-speaking .shatter-avatar {
    animation: shatter-speaking-wobble 2.4s ease-in-out infinite;
    transform-origin: 50% 50%;
}
.shatter-card-speaking .shatter-avatar::after {
    content: '';
    position: absolute;
    inset: -4px;
    border-radius: 50%;
    border: 2px solid var(--shatter-link);
    pointer-events: none;
    animation: shatter-speaking-pulse 1.6s ease-in-out infinite;
}
@keyframes shatter-speaking-pulse {
    0%, 100% { opacity: 0.35; transform: scale(1); }
    50%      { opacity: 0.85; transform: scale(1.08); }
}
@keyframes shatter-speaking-wobble {
    0%   { transform: scale(1)    rotate(0deg);   }
    12%  { transform: scale(1.35) rotate(0deg);   }
    24%  { transform: scale(1.35) rotate(-12deg); }
    36%  { transform: scale(1.35) rotate(10deg);  }
    48%  { transform: scale(1.35) rotate(-8deg);  }
    60%  { transform: scale(1.35) rotate(6deg);   }
    72%  { transform: scale(1.35) rotate(0deg);   }
    85%  { transform: scale(1)    rotate(0deg);   }
    100% { transform: scale(1)    rotate(0deg);   }
}
@media (prefers-reduced-motion: reduce) {
    .shatter-card-speaking .shatter-avatar { animation: none; }
}
.shatter-profile-avatar {
    flex-shrink: 0;
    width: 96px;
    height: 96px;
    border-radius: 50%;
    overflow: hidden;
    background: #2a3640;
    display: flex;
    align-items: center;
    justify-content: center;
}
.shatter-profile-avatar img { width: 100%; height: 100%; object-fit: cover; object-position: top; }
.shatter-profile-info { flex: 1; min-width: 0; }
.shatter-profile-name { margin: 0 0 2px; font-size: 22px; font-weight: 800; }
.shatter-profile-handle { color: var(--shatter-handle); font-size: 14px; margin-bottom: 8px; }
.shatter-profile-bio { margin: 8px 0; white-space: pre-wrap; }

/* Admin-only inline edit on the actor profile: pencil button next to Motivation +
   Description headers, with a sibling textarea + Save/Cancel form that toggles in. */
.shatter-profile-meta-row { position: relative; }
.shatter-profile-bio-row  { margin: 8px 0; }
.shatter-profile-meta-head {
    display: flex;
    align-items: center;
    gap: 6px;
}
.shatter-profile-meta-head > h3 { margin: 0; }
.shatter-admin-edit-meta-btn {
    border: 0;
    background: rgba(255,255,255,0.06);
    color: var(--shatter-muted);
    width: 24px;
    height: 24px;
    border-radius: 50%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    transition: background 0.15s ease, color 0.15s ease;
}
.shatter-admin-edit-meta-btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
.shatter-profile-bio-row .shatter-admin-edit-meta-btn {
    position: absolute;
    top: 4px;
    right: 4px;
}
.shatter-profile-meta-empty { color: var(--shatter-muted); }
.shatter-profile-meta-edit textarea {
    width: 100%;
    box-sizing: border-box;
    background: rgba(255,255,255,0.04);
    border: 1px solid var(--shatter-card-border);
    border-radius: 8px;
    color: var(--shatter-text);
    padding: 8px 10px;
    font: inherit;
    line-height: 1.4;
    resize: vertical;
    margin: 4px 0 6px;
}
.shatter-profile-meta-edit textarea:focus {
    outline: none;
    border-color: var(--shatter-link);
}
.shatter-profile-meta-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}
.shatter-profile-meta-cancel {
    background: transparent;
    color: var(--shatter-muted);
    border: 1px solid var(--shatter-card-border);
    border-radius: 999px;
    padding: 4px 12px;
    cursor: pointer;
    font: inherit;
}
.shatter-profile-meta-cancel:hover { color: #fff; border-color: rgba(255,255,255,0.4); }
.shatter-profile-meta-status { font-size: 12px; color: var(--shatter-muted); }
.shatter-profile-meta-status.error { color: #d54e21; }
.shatter-profile-shows { margin: 10px 0 8px; }
.shatter-profile-shows-label { color: var(--shatter-muted); font-size: 13px; margin-right: 6px; }
.shatter-show-badge {
    display: inline-block;
    background: rgba(29, 155, 240, 0.1);
    color: var(--shatter-link);
    padding: 3px 10px;
    border-radius: 999px;
    font-size: 13px;
    margin: 2px 4px 2px 0;
}
.shatter-show-badge:hover { background: rgba(29, 155, 240, 0.2); text-decoration: none; }
.shatter-profile-stats { color: var(--shatter-muted); font-size: 13px; margin-top: 8px; display: flex; gap: 16px; }
.shatter-profile-stats strong { color: var(--shatter-text); }

@media (max-width: 600px) {
    .shatter-media-card { flex-direction: column; }
    .shatter-media-thumb { width: 100%; height: 160px; }
    .shatter-profile { flex-direction: column; align-items: center; text-align: center; }
    .shatter-profile-shows, .shatter-profile-stats { justify-content: center; }
}

/* Below 480px the card's fixed overhead (avatar + padding + gap) starts eating
   the content column. Tighten avatar, gap, and padding so the timeline still
   has breathing room when embedded in a narrow sidebar. 480px aligns with the
   existing logo-shrink + volume-slider-hide breakpoints elsewhere in this file. */
@media (max-width: 480px) {
    .shatter-card { padding: 10px 10px; gap: 8px; }
    .shatter-avatar { width: 36px; height: 36px; }
    .shatter-header-inner { padding: 8px 10px; gap: 10px; }
    body.shatter-embed .shatter-header-inner { padding: 6px 8px; gap: 6px; }
}

/* iPhone-range tightening (covers 360px small-Android floor up through iPhone 14
   Pro Max at 430px). Buys back ~30px of content room by shrinking card padding,
   avatar, and the 12px sibling margins between stat / admin chrome buttons.
   .shatter-card-foot's flex-wrap is the primary defense against overflow; this
   layers on top so wrapping happens later (or not at all) for non-admin viewers. */
@media (max-width: 430px) {
    .shatter-card { padding: 8px 8px; gap: 6px; }
    .shatter-avatar { width: 32px; height: 32px; }
    .shatter-avatar-fallback { font-size: 14px; }
    .shatter-stat { margin-left: 8px; }
    .shatter-admin-reply-btn { margin-left: 6px; }
}

/* Embed iframes are sized externally by the host page — when the column is
   narrower than the natural Shatter min-content width (rare wide media,
   long unbreakable strings), clip rather than letting the iframe show a
   horizontal scrollbar. Also applied to standalone /shatter as a safety net:
   without it, any single overflowing child propagates past .shatter-main,
   widens the body, and triggers iOS Safari's whole-page auto-zoom-out. */
body.shatter-body,
body.shatter-embed { overflow-x: hidden; }

/* Admin-only "Reply as actor" button + modal. Only present in DOM when the
   server-render detected an admin viewer, so non-admins never see these. */
.shatter-admin-reply-btn {
    margin-left: 12px;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--shatter-muted);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.shatter-admin-reply-btn:hover { color: var(--shatter-link); background: rgba(29, 155, 240, 0.1); }

/* Phase 10.3: admin Delete button — same shape as Reply, but with a destructive hover. */
.shatter-admin-delete-btn {
    margin-left: 4px;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--shatter-muted);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.shatter-admin-delete-btn:hover {
    color: #f4212e;
    background: rgba(244, 33, 46, 0.12);
}
.shatter-admin-delete-btn:disabled {
    opacity: 0.5;
    cursor: default;
}

.shatter-admin-edit-btn {
    margin-left: 4px;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--shatter-muted);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}
.shatter-admin-edit-btn:hover {
    color: var(--shatter-text);
    background: rgba(255, 255, 255, 0.05);
}
.shatter-admin-edit-btn:disabled {
    opacity: 0.5;
    cursor: default;
}

/* Admin Like / Unlike button. Same 28×28 circle shape as the other admin buttons,
   with a Twitter-style red hover. data-liked="1" paints the heart filled red.
   Endpoint is public; UI is admin-gated for now via the cfg.isAdmin render check. */
.shatter-admin-like-btn {
    margin-left: 4px;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--shatter-muted);
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: transform 120ms ease, color 120ms ease, background 120ms ease;
}
.shatter-admin-like-btn:hover {
    color: #f91880;
    background: rgba(249, 24, 128, 0.12);
}
.shatter-admin-like-btn[data-liked="1"] {
    color: #f91880;
}
.shatter-admin-like-btn:active {
    transform: scale(0.92);
}
.shatter-admin-like-btn:disabled {
    opacity: 0.5;
    cursor: default;
}
/* Stat next to plays/visits/interactions. Picks up the red tint from the count
   color when the viewing user has liked the shat — but for the admin gate we
   currently rely on the button itself; the stat stays muted to avoid implying
   non-admin viewers can act on it. */
.shatter-stat-likes { /* inherits from .shatter-stat */ }

.shatter-edit-textarea {
    width: 100%;
    min-height: 80px;
    padding: 8px;
    border: 1px solid var(--shatter-card-border);
    border-radius: 6px;
    background: rgba(255, 255, 255, 0.04);
    color: var(--shatter-text);
    font-family: inherit;
    font-size: 14px;
    line-height: 1.4;
    resize: vertical;
    box-sizing: border-box;
}
.shatter-edit-actions {
    margin-top: 8px;
    display: flex;
    gap: 8px;
    align-items: center;
    flex-wrap: wrap;
}
.shatter-edit-save {
    background: var(--shatter-link);
    color: #fff;
    border: 0;
    padding: 5px 14px;
    border-radius: 999px;
    cursor: pointer;
    font-size: 13px;
    font-family: inherit;
    font-weight: 600;
}
.shatter-edit-save:hover { background: #1a8cd8; }
.shatter-edit-cancel {
    background: transparent;
    color: var(--shatter-muted);
    border: 1px solid var(--shatter-card-border);
    padding: 4px 12px;
    border-radius: 999px;
    cursor: pointer;
    font-size: 13px;
    font-family: inherit;
}
.shatter-edit-cancel:hover { color: var(--shatter-text); }
.shatter-edit-save:disabled,
.shatter-edit-cancel:disabled {
    opacity: 0.5;
    cursor: default;
    pointer-events: none;
}
.shatter-edit-status {
    font-size: 12px;
    color: var(--shatter-muted);
}
.shatter-edit-status.error { color: #f4212e; }

/* Phase 10.1: "Show more" link inside truncated body text. */
.shatter-text-more {
    color: var(--shatter-link);
    font-weight: 500;
    white-space: nowrap;
}
.shatter-text-more:hover { text-decoration: underline; }

.shatter-modal {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}
.shatter-modal[hidden] { display: none; }
.shatter-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
}
.shatter-modal-card {
    position: relative;
    background: var(--shatter-card);
    border: 1px solid var(--shatter-card-border);
    border-radius: 14px;
    padding: 20px;
    width: 100%;
    max-width: 480px;
    color: var(--shatter-text);
    box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}
.shatter-modal-card h2 { margin: 0 0 8px; font-size: 18px; }
.shatter-modal-target { color: var(--shatter-muted); font-size: 13px; margin: 0 0 16px; }
.shatter-modal-card label {
    display: block;
    margin: 12px 0;
    color: var(--shatter-muted);
    font-size: 13px;
}
.shatter-modal-card select,
.shatter-modal-card textarea {
    display: block;
    width: 100%;
    margin-top: 4px;
    background: var(--shatter-bg);
    color: var(--shatter-text);
    border: 1px solid var(--shatter-card-border);
    border-radius: 6px;
    padding: 8px 10px;
    font: inherit;
    box-sizing: border-box;
}
.shatter-modal-card textarea { resize: vertical; min-height: 70px; }

/* Phase 25: research-preset picker is a utility row beneath the guidance textarea —
   tighter spacing + smaller label so it doesn't compete visually with the primary
   inputs. Picking a preset inserts the [grokResearch preset='NAME'] shortcode at the
   guidance textarea's cursor, then resets the dropdown. */
.shatter-modal-grok-preset { margin-top: 6px !important; font-size: 12px; }
.shatter-modal-grok-preset select { margin-top: 2px; }
/* Phase 31 / 38: admin can drop/paste/browse an image inside the Generate Shat modal. */
.shatter-modal-image-row { margin-top: 12px; }
.shatter-modal-image-row > label {
    display: block;
    font-size: 12px;
    margin-bottom: 6px;
    color: var(--shatter-muted, #888);
}
.shatter-modal-image-drop {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 96px;
    padding: 18px 16px;
    border: 2px dashed var(--shatter-card-border, #333);
    border-radius: 10px;
    background: rgba(255,255,255,0.02);
    transition: background 0.15s ease, border-color 0.15s ease;
    outline: none;
    cursor: pointer;
}
.shatter-modal-image-drop:hover { background: rgba(255,255,255,0.04); }
.shatter-modal-image-drop:focus { border-color: var(--shatter-link, #1da1f2); }
.shatter-modal-image-drop.is-hover {
    border-color: var(--shatter-link, #1da1f2);
    background: rgba(29,161,242,0.10);
}
.shatter-modal-image-drop.is-uploading { opacity: 0.6; pointer-events: none; }
.shatter-modal-image-drop-hint {
    margin: 0;
    font-size: 13px;
    color: var(--shatter-muted, #888);
    text-align: center;
    line-height: 1.5;
}
.shatter-modal-image-drop-hint::before {
    content: '📎';
    display: block;
    font-size: 22px;
    margin-bottom: 6px;
    opacity: 0.6;
}
.shatter-modal-image-browse {
    background: none;
    border: none;
    padding: 0;
    color: var(--shatter-link, #1da1f2);
    text-decoration: underline;
    cursor: pointer;
    font: inherit;
}
.shatter-modal-image-browse:hover { color: #fff; }

/* Specificity fix: only apply layout when NOT hidden, so the hidden attribute's
   UA-stylesheet `display: none` actually takes effect. Previously the bare class
   rule `display: inline-block` was overriding the hidden attribute and leaving
   the × button visible even when no image was attached. */
.shatter-modal-image-preview:not([hidden]) {
    position: relative;
    display: block;
    max-width: 100%;
    padding-right: 80px; /* room for the absolute-positioned Clear-all button */
}
.shatter-modal-image-thumbs {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}
.shatter-modal-image-thumb {
    position: relative;
    width: 96px;
    height: 96px;
    border-radius: 8px;
    overflow: hidden;
    border: 1px solid var(--shatter-card-border, #333);
    background: var(--shatter-card, #16202a);
}
.shatter-modal-image-thumb img,
.shatter-modal-image-thumb video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.shatter-modal-image-thumb-video-badge {
    position: absolute;
    left: 4px;
    bottom: 4px;
    background: rgba(0,0,0,0.65);
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.08em;
    padding: 2px 6px;
    border-radius: 4px;
    pointer-events: none;
}
.shatter-modal-image-thumb-remove {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    border: 0;
    background: rgba(0,0,0,0.6);
    color: #fff;
    font-size: 14px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}
.shatter-modal-image-thumb-remove:hover { background: rgba(0,0,0,0.85); }
.shatter-modal-image-remove {
    position: absolute;
    top: 6px;
    right: 6px;
    height: 28px;
    padding: 0 12px;
    border: 0;
    border-radius: 14px;
    background: rgba(0,0,0,0.7);
    color: #fff;
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    white-space: nowrap;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s ease;
}
.shatter-modal-image-remove:hover { background: rgba(220,50,50,0.9); }
.shatter-modal-actions {
    display: flex;
    gap: 8px;
    align-items: center;
    margin-top: 16px;
}
.shatter-modal-submit,
.shatter-modal-cancel {
    background: transparent;
    color: var(--shatter-text);
    border: 1px solid var(--shatter-card-border);
    padding: 8px 16px;
    border-radius: 999px;
    cursor: pointer;
    font: inherit;
}
.shatter-modal-submit {
    background: var(--shatter-link);
    color: #fff;
    border-color: var(--shatter-link);
    font-weight: 600;
}
.shatter-modal-submit:disabled {
    opacity: 0.6;
    cursor: default;
}
.shatter-modal-cancel:hover { background: rgba(255, 255, 255, 0.05); }
#shatter-modal-status { color: var(--shatter-muted); font-size: 13px; margin-left: auto; }
#shatter-modal-status.error { color: #f4212e; }

/* Phase 26: Shatters List page (actor directory) — Twitter-style profile cards in a
   single stacked column. Each card is a clickable <a> that navigates to the actor's
   full Shatter profile. Banner background + avatar overlap + metrics overlay match the
   look of the individual profile page (Phase 4) for visual continuity. */
.shatters-list { list-style: none; padding: 0; margin: 0; }
.shatters-list-item { list-style: none; padding: 0 16px; margin: 0 0 16px; }
.shatters-list-item:first-child { padding-top: 16px; }

.shatters-list-card {
    display: block;
    background: var(--shatter-card);
    border: 1px solid var(--shatter-card-border);
    border-radius: 14px;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.shatters-list-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
    border-color: var(--shatter-link);
    text-decoration: none;
}

.shatters-list-banner {
    position: relative;
    width: 100%;
    aspect-ratio: 3 / 1;
    background: linear-gradient(135deg, #1d3a5c, #2a3640);
    background-size: cover;
    background-position: center;
}
.shatters-list-metrics {
    position: absolute;
    top: 12px;
    right: 12px;
    display: flex;
    gap: 12px;
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(4px);
    padding: 6px 12px;
    border-radius: 999px;
    font-size: 13px;
    color: #fff;
}
.shatters-list-metric strong { color: var(--shatter-link); margin-right: 2px; }

.shatters-list-body {
    display: flex;
    gap: 16px;
    padding: 14px 18px 18px;
    align-items: flex-start;
}
.shatters-list-avatar {
    flex-shrink: 0;
    width: 88px;
    height: 88px;
    border-radius: 50%;
    background-size: cover;
    background-position: center;
    background-color: #2a3640;
    border: 4px solid var(--shatter-card);
    margin-top: -52px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--shatter-text);
    font-size: 36px;
    font-weight: 700;
    /* Phase 26.1: paint above the banner. The banner has position:relative for its
       metrics overlay, which makes it a positioned element that paints above static
       siblings regardless of DOM order. Match its stacking with relative + z-index
       so the avatar (later in DOM) wins. */
    position: relative;
    z-index: 1;
}
.shatters-list-text { flex: 1; min-width: 0; padding-top: 4px; }
.shatters-list-name {
    font-size: 22px;
    font-weight: 700;
    color: var(--shatter-text);
    line-height: 1.2;
}
.shatters-list-handle {
    color: var(--shatter-handle);
    font-weight: 400;
    font-size: 14px;
    margin-left: 6px;
}
.shatters-list-bio {
    margin: 6px 0 0;
    color: var(--shatter-muted);
    font-size: 14px;
    line-height: 1.45;
}

@media (max-width: 600px) {
    .shatters-list-metrics { font-size: 11px; gap: 8px; padding: 4px 8px; }
    .shatters-list-avatar { width: 72px; height: 72px; margin-top: -40px; font-size: 30px; }
    .shatters-list-name { font-size: 18px; }
}

/* Phase 44 (updated Phase 46 / Phase 48): comic dialogue bubble. The chat bubble
   slides up at bottom-center; a large 480×480 transparent-PNG actor cutout (full
   resolution, sourced from data-full-src) rises from the bottom edge of the screen
   and is centered horizontally on the bubble's left edge. The portrait is a
   SIBLING of .pm-alert (not a child) because .pm-alert has transform:translateX(-50%)
   — per the CSS Transforms spec that turns it into the containing block for any
   position:fixed descendants, which would trap the portrait inside the bubble's
   coords instead of anchoring it to the viewport. Z-index hierarchy: layer at 2000
   sits above .shatter-lightbox (z:1000), so the chat panel stays visible over the
   image lightbox; bubble at z:2 inside the layer; portrait at z:1 inside the layer.
   Layers on top of the existing speaking-card tint, avatar wobble, ring pulse, and
   karaoke; doesn't replace any of them. */

#pm-alert-layer {
    position: fixed;
    inset: 0;
    pointer-events: none;
    /* Above .shatter-lightbox (z:1000) so the chat panel stays visible when the
       user opens an image lightbox during playback. */
    z-index: 2000;
}

.pm-alert {
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    width: min(80vw, 820px);
    padding: 14px 20px;
    background: #fffdf2;
    border: 4px solid #1a1a1a;
    border-radius: 16px;
    box-shadow: 0 10px 0 #1a1a1a, 0 18px 40px rgba(0, 0, 0, 0.45);
    display: flex;
    gap: 16px;
    align-items: center;
    pointer-events: auto;
    isolation: isolate;
    z-index: 2;
    animation: pm-slide-in-bc 420ms cubic-bezier(.2, 1.5, .4, 1) both;
}

.pm-alert.pm-leaving {
    animation: pm-slide-out-bc 260ms ease-in forwards;
}

/* Portrait is a SIBLING of .pm-alert (see comment block above). Anchored to the
   viewport bottom-left area: center horizontally on the bubble's left edge by
   shifting from screen-center by (half-bubble-width + half-portrait-width). The
   --pm-portrait-x custom property keeps the X translate consistent across the
   static rule and the animation keyframes. */
.pm-alert__portrait {
    --pm-portrait-x: calc(-1 * min(40vw, 410px) - 240px);
    position: fixed;
    bottom: 0;
    left: 50%;
    transform: translateX(var(--pm-portrait-x));
    width: 480px;
    height: 480px;
    z-index: 1;
    pointer-events: none;
    animation: pm-portrait-rise 420ms cubic-bezier(.2, 1.5, .4, 1) both;
}

.pm-alert__portrait.pm-leaving {
    animation: pm-portrait-fall 260ms ease-in forwards;
}

.pm-alert__portrait img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    /* Portrait sources show the actor's head at the top of the frame, not the torso. */
    object-position: top;
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    /* drop-shadow follows the transparent PNG's alpha silhouette — a true
       cartoon-cutout shadow under the actor, not a rectangular box. */
    filter: drop-shadow(0 6px 8px rgba(0, 0, 0, 0.5));
}

.pm-alert__body {
    flex: 1 1 auto;
    min-width: 0;
}

.pm-alert__header {
    display: flex;
    flex-flow: row nowrap;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 4px;
    min-width: 0;
}

.pm-alert__name {
    /* Color-emoji font fallbacks at the end of the stack — CSS picks per-glyph,
       so latin text renders in Comic Neue while emoji codepoints fall through
       to the OS's color-emoji font (matching how the timeline body renders). */
    font-family: 'Comic Neue', system-ui, sans-serif,
                 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji';
    font-weight: 700;
    font-size: 15px;
    letter-spacing: 1.2px;
    text-transform: uppercase;
    color: #d12d2d;
    text-shadow: 0 2px 0 rgba(0, 0, 0, 0.08);
    flex: 0 0 auto;
    white-space: nowrap;
}

/* The 3-line clamp — line-height (1.18) MUST match the multiplier in max-height. */
.pm-alert__text {
    /* Same per-glyph color-emoji fallback story as .pm-alert__name above —
       latin text in Bangers, emoji codepoints in the OS color-emoji font. */
    font-family: 'Bangers', system-ui, sans-serif,
                 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji';
    font-size: clamp(22px, 2.6vw, 32px);
    line-height: 1.18;
    letter-spacing: 1.2px;
    color: #1a1a1a;
    max-height: calc(1.18em * 3);
    overflow: hidden;
    /* Phase 50: prefer word-boundary wrapping (real text-node spaces between letter
       spans give the browser soft-wrap opportunities); only break mid-word as a
       last resort when a single unbroken run is wider than the container. */
    word-wrap: break-word;
    overflow-wrap: break-word;
}

.pm-alert__text-inner {
    /* JS sets transform: translateY(-N px) to scroll content up as text grows past 3 lines. */
    transition: transform 0.12s ease-out;
}

.pm-letter {
    display: inline-block;
    opacity: 0;
    animation: pm-letter-rise 200ms cubic-bezier(.2, 1.4, .4, 1) forwards;
    transform-origin: 50% 80%;
    will-change: transform, opacity;
}

.pm-letter.pm-space { width: .35em; }

/* Voice-synth loading state inside the comic bubble. Replaced by karaoke
   letters once audio.onplaying fires + pmAppendLetter runs. Bars pulse in
   a 4-stage equalizer pattern so it reads as "voice loading," not a generic
   spinner. */
.pm-alert__loading {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
    padding: 12px 0 12px 4px;
}
.pm-alert__loading-spinner {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    height: 28px;
}
.pm-alert__loading-bar {
    display: inline-block;
    width: 5px;
    height: 22px;
    background: #d12d2d;
    border-radius: 2px;
    transform-origin: center;
    animation: pm-alert-eq 0.9s ease-in-out infinite;
}
.pm-alert__loading-bar:nth-child(2) { animation-delay: 0.15s; }
.pm-alert__loading-bar:nth-child(3) { animation-delay: 0.3s;  }
.pm-alert__loading-bar:nth-child(4) { animation-delay: 0.45s; }
@keyframes pm-alert-eq {
    0%, 100% { transform: scaleY(0.3); }
    50%      { transform: scaleY(1);   }
}
.pm-alert__loading-text {
    font-family: 'Bangers', system-ui, sans-serif;
    font-size: clamp(18px, 2.4vw, 28px);
    letter-spacing: 1.5px;
    color: #1a1a1a;
}

/* Phase 51: each word is an atomic inline-block. white-space: nowrap prevents the
   browser from wrapping between the inline-block .pm-letter children inside —
   the only soft-wrap opportunities in the bubble text are now the text-node
   spaces between .pm-word elements. Very long single words overflow-clip on
   the right rather than break mid-character. */
.pm-word {
    display: inline-block;
    white-space: nowrap;
}

/* Slide straight up from below screen, preserving the static translateX(-50%)
   centering at every keyframe. */
@keyframes pm-slide-in-bc {
    0%   { transform: translate(-50%, 110%) scale(0.9);  opacity: 0; }
    70%  { transform: translate(-50%, -2%)  scale(1.02); opacity: 1; }
    100% { transform: translate(-50%, 0)    scale(1);    opacity: 1; }
}

@keyframes pm-slide-out-bc {
    0%   { transform: translate(-50%, 0)    scale(1);   opacity: 1; }
    100% { transform: translate(-50%, 110%) scale(0.9); opacity: 0; }
}

/* Portrait rises from below the screen, preserving the --pm-portrait-x X offset
   at every keyframe so the horizontal centering on the bubble's left edge stays
   constant while only Y animates. */
@keyframes pm-portrait-rise {
    0%   { transform: translateX(var(--pm-portrait-x)) translateY(110%); opacity: 0; }
    70%  { transform: translateX(var(--pm-portrait-x)) translateY(-3%);  opacity: 1; }
    100% { transform: translateX(var(--pm-portrait-x)) translateY(0);    opacity: 1; }
}

@keyframes pm-portrait-fall {
    0%   { transform: translateX(var(--pm-portrait-x)) translateY(0);    opacity: 1; }
    100% { transform: translateX(var(--pm-portrait-x)) translateY(110%); opacity: 0; }
}

@keyframes pm-letter-rise {
    0%   { transform: translateY(8px)  scale(0.5);  opacity: 0; }
    60%  { transform: translateY(-2px) scale(1.15); opacity: 1; }
    100% { transform: translateY(0)    scale(1);    opacity: 1; }
}

@media (max-width: 600px) {
    /* On mobile the bubble spans edge-to-edge (left:12 + right:12 with auto width),
       so we drop the translateX(-50%) centering and use the mobile-only keyframes
       that don't carry an X offset. */
    .pm-alert {
        bottom: 12px;
        left: 12px;
        right: 12px;
        width: auto;
        transform: none;
        padding: 10px 14px;
        gap: 12px;
        animation-name: pm-slide-in-bc-mobile;
    }
    .pm-alert.pm-leaving { animation-name: pm-slide-out-bc-mobile; }
    /* Portrait shrinks to 280×280 on mobile and anchors near the left edge of
       the viewport (with a small left margin), since the bubble's left edge is
       only 12px from the viewport edge. We drop --pm-portrait-x's X math and
       switch to mobile-only keyframes that animate Y only. */
    .pm-alert__portrait {
        width: 280px;
        height: 280px;
        /* Halfway between the original `-140px` (fully off-screen left) and
           the prior `10px` (fully on-screen) — leaves the actor noticeably
           visible while bleeding the head/shoulders past the bubble's left
           edge so the bubble's content area isn't crowded out. */
        left: -65px;
        transform: none;
        animation-name: pm-portrait-rise-mobile;
    }
    .pm-alert__portrait.pm-leaving { animation-name: pm-portrait-fall-mobile; }
}

/* Landscape phones: viewport-height is constrained, so the 480px desktop
   portrait would eat the whole screen. Halve it to 240×240 and recompute the
   --pm-portrait-x offset (half-width 120 instead of 240) so the portrait's
   CENTER stays on the bubble's LEFT EDGE — same alignment paradigm as desktop,
   just smaller. left/transform/animation inherit from the desktop defaults. */
@media (orientation: landscape) and (max-height: 500px) {
    .pm-alert__portrait {
        --pm-portrait-x: calc(-1 * min(40vw, 410px) - 120px);
        width: 240px;
        height: 240px;
    }
}

@keyframes pm-slide-in-bc-mobile {
    0%   { transform: translateY(110%) scale(0.9);  opacity: 0; }
    70%  { transform: translateY(-2%)  scale(1.02); opacity: 1; }
    100% { transform: translateY(0)    scale(1);    opacity: 1; }
}

@keyframes pm-slide-out-bc-mobile {
    0%   { transform: translateY(0)    scale(1);   opacity: 1; }
    100% { transform: translateY(110%) scale(0.9); opacity: 0; }
}

@keyframes pm-portrait-rise-mobile {
    0%   { transform: translateY(110%); opacity: 0; }
    70%  { transform: translateY(-3%);  opacity: 1; }
    100% { transform: translateY(0);    opacity: 1; }
}

@keyframes pm-portrait-fall-mobile {
    0%   { transform: translateY(0);    opacity: 1; }
    100% { transform: translateY(110%); opacity: 0; }
}

/* Phase 52: autoplay bar — sits above the feed in all three feed views (global,
   thread, per-actor). data-state on the wrapper drives which controls are visible:
   "idle"    → big Autoplay start button only
   "playing" → transport row (prev / pause / next / stop) with ⏸ glyph on toggle
   "paused"  → same row with ▶ glyph on toggle */

/* Phase 54 (updated Phase 54.2): autoplay bar is now a flex child INSIDE
   .shatter-volume-control (the bottom-left fixed dock), not a standalone fixed
   pill. The parent provides the glass-pill chrome and z-index; this element
   just lays its idle button / transport row out horizontally. */
.shatter-autoplay-bar {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
}

/* Phase 54.3: three-way visibility driven by data-state. The bar shows EXACTLY
   one of the three faces:
     idle    → .shatter-autoplay-start (▶ Autoplay)
     single  → .shatter-autoplay-solo-stop (⏹ STOP — single-card manual playback)
     playing/paused → .shatter-autoplay-controls (full transport row) */
.shatter-autoplay-bar[data-state="idle"] .shatter-autoplay-controls,
.shatter-autoplay-bar[data-state="idle"] .shatter-autoplay-solo-stop { display: none; }
.shatter-autoplay-bar[data-state="playing"] .shatter-autoplay-start,
.shatter-autoplay-bar[data-state="playing"] .shatter-autoplay-solo-stop,
.shatter-autoplay-bar[data-state="paused"]  .shatter-autoplay-start,
.shatter-autoplay-bar[data-state="paused"]  .shatter-autoplay-solo-stop { display: none; }
.shatter-autoplay-bar[data-state="single"] .shatter-autoplay-start,
.shatter-autoplay-bar[data-state="single"] .shatter-autoplay-controls { display: none; }

.shatter-autoplay-start {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    border: 0;
    border-radius: 999px;
    background: var(--shatter-link);
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
    transition: transform 0.1s ease, background 0.1s ease;
    font-family: inherit;
}
.shatter-autoplay-start:hover  { background: #1a85d0; transform: scale(1.03); }
.shatter-autoplay-start:active { transform: scale(0.98); }
.shatter-autoplay-icon         { font-size: 14px; line-height: 1; }

/* Phase 54.3: solo STOP — same pill shape as ▶ Autoplay but destructive red so
   the user instantly reads "halt the talking actor". Shown only in data-state="single",
   i.e. when a manual single-card play is active (NOT during autoplay). */
.shatter-autoplay-solo-stop {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 16px;
    border: 0;
    border-radius: 999px;
    background: #d12d2d;
    color: #fff;
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
    transition: transform 0.1s ease, background 0.1s ease;
    font-family: inherit;
}
.shatter-autoplay-solo-stop:hover  { background: #b22020; transform: scale(1.03); }
.shatter-autoplay-solo-stop:active { transform: scale(0.98); }

.shatter-autoplay-controls {
    display: flex;
    align-items: center;
    gap: 4px;
    justify-content: center;
}
.shatter-autoplay-controls button {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    border: 0;
    background: var(--shatter-card);
    color: var(--shatter-text);
    font-size: 15px;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-family: inherit;
    padding: 0;
}
.shatter-autoplay-controls button:hover { background: rgba(255,255,255,0.08); }

/* The play/pause toggle is the visual centerpiece: slightly bigger and
   link-colored, with the glyph drawn via ::before so data-state swaps it
   without touching markup. */
.shatter-autoplay-toggle {
    background: var(--shatter-link) !important;
    color: #fff !important;
    width: 40px !important;
    height: 40px !important;
    font-size: 0 !important;
}
.shatter-autoplay-toggle::before { font-size: 18px; }
.shatter-autoplay-bar[data-state="playing"] .shatter-autoplay-toggle::before { content: "\23F8"; } /* ⏸ */
.shatter-autoplay-bar[data-state="paused"]  .shatter-autoplay-toggle::before { content: "\25B6"; } /* ▶ */

/* Phase 53: card pop-in animation. .shatter-card lives in its full layout box at
   all times — we use opacity + transform (GPU-composited, no layout reflow) for
   the hidden state, so siblings never shift when cards reveal. The hidden state
   is scoped to html.shatter-pop-enabled (set by the inline <head> script) so that
   if JS fails to load before paint, cards stay visible — graceful degradation.
   No conflict with .shatter-card-focused (background-only animation) or
   .shatter-card-speaking (background tint + child avatar wobble). */
html.shatter-pop-enabled .shatter-card {
    opacity: 0;
    transform: translateY(18px) scale(0.96);
    transition: opacity 0.45s cubic-bezier(.2, .8, .3, 1),
                transform 0.55s cubic-bezier(.34, 1.4, .5, 1);
    will-change: opacity, transform;
}
html.shatter-pop-enabled .shatter-card.is-revealed {
    opacity: 1;
    transform: translateY(0) scale(1);
}
/* Drop the compositing hint after the pop-in settles, so 200+ revealed cards
   don't permanently keep GPU layers around. */
html.shatter-pop-enabled .shatter-card.is-revealed.is-settled {
    will-change: auto;
}

@media (prefers-reduced-motion: reduce) {
    html.shatter-pop-enabled .shatter-card {
        transition: none;
        opacity: 1;
        transform: none;
        will-change: auto;
    }
}

/* Phase 54: master volume control. Inline in the Shatter header — the header
   provides its own glass background, so this is just a flex group of buttons.
   Scales both Tone.js SFX (pmSfx.masterVolume) and the ElevenLabs voice audio. */
.shatter-volume-control {
    display: flex;
    align-items: center;
    gap: 10px;
    flex: 1 1 auto;
    justify-content: center;
    min-width: 0;
}
/* Tight right-side cluster: people-icon + Generate Shat. flex-shrink: 0 keeps
   them rigid so the centered volume-control gives ground first on narrow screens. */
.shatter-header-right {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}
/* Autoplay buttons are always icon-only (aria-label keeps screen-reader meaning). */
.shatter-autoplay-label { display: none; }
/* Phase 54.2: thin vertical separator between the volume half and the autoplay
   half of the combined dock. Hidden on mobile where the dock wraps to two rows. */
.shatter-control-divider {
    width: 1px;
    align-self: stretch;
    background: rgba(255, 255, 255, 0.18);
    margin: 0 2px;
}
.shatter-volume-btn {
    width: 36px;
    height: 36px;
    border: 0;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.05);
    color: var(--shatter-text);
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0;
}
.shatter-volume-btn:hover { background: rgba(255, 255, 255, 0.1); }
.shatter-volume-control[data-muted="false"] .shatter-volume-btn::before { content: "\1F50A"; } /* 🔊 */
.shatter-volume-control[data-muted="true"]  .shatter-volume-btn::before { content: "\1F507"; } /* 🔇 */
.shatter-volume-slider {
    width: 120px;
    accent-color: var(--shatter-link);
    cursor: pointer;
}
/* Mobile: tighten the in-header audio controls. Slider and autoplay text labels
   are dropped — mute toggle + autoplay icon buttons stay reachable. The slider's
   persisted volume value (localStorage shatter_volume) still applies; tap mute
   for binary on/off. Full controls return at desktop widths. */
@media (max-width: 480px) {
    .shatter-volume-slider     { display: none; }
    .shatter-control-divider   { display: none; }
}

/* Phase 56: "X new shats" ribbon at the top of <main> on every Shatter page.
   Full-width, accent-colored, single-line — clicking navigates back to the main
   timeline. Sticks to the top of <main> as the page scrolls so the nudge stays
   visible until the user acts on it. */
.shatter-new-ribbon {
    display: block;
    width: 100%;
    padding: 12px 16px;
    margin: 0;
    border: 0;
    border-bottom: 1px solid var(--shatter-card-border);
    background: var(--shatter-link);
    color: #fff;
    font: inherit;
    font-weight: 600;
    text-align: center;
    cursor: pointer;
    position: sticky;
    top: var(--shatter-header-height);
    z-index: 10;
}
.shatter-new-ribbon[hidden] { display: none; }
.shatter-new-ribbon:hover { filter: brightness(1.1); }
.shatter-new-ribbon-count { font-weight: 800; }

/* Phase 56: daily/weekly sub-toggle inside an active topic preset tab. Compact
   pill row centered above the research feed. */
.shatter-window-toggle {
    display: inline-flex;
    margin: 12px auto;
    padding: 2px;
    border: 1px solid var(--shatter-card-border);
    border-radius: 999px;
    background: var(--shatter-card);
    align-self: center;
}
.shatter-main .shatter-window-toggle {
    display: flex;
    width: fit-content;
    margin: 12px auto;
}
.shatter-window-tab {
    padding: 6px 18px;
    border-radius: 999px;
    color: var(--shatter-muted);
    font-weight: 600;
    font-size: 13px;
    line-height: 1;
    text-decoration: none;
}
.shatter-window-tab:hover { color: var(--shatter-text); text-decoration: none; }
.shatter-window-tab-active {
    background: var(--shatter-link);
    color: #fff;
}
.shatter-window-tab-active:hover { color: #fff; }

/* Phase 56: admin "Update Research Now" action row above the research feed. */
.shatter-research-admin-actions {
    padding: 8px 16px 0 16px;
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
}
.shatter-update-research-btn {
    background: rgba(29,161,242,0.10);
    border: 1px solid var(--shatter-card-border);
    color: var(--shatter-link);
    padding: 6px 14px;
    border-radius: 14px;
    font-size: 13px;
    cursor: pointer;
    font-family: inherit;
}
.shatter-update-research-btn:hover:not(:disabled) { background: rgba(29,161,242,0.20); }
.shatter-update-research-btn:disabled { opacity: 0.6; cursor: progress; }
.shatter-update-research-status {
    font-size: 12px;
    color: var(--shatter-muted);
}

/* Shatter Stats sidebar — only present on the main "For You" feed. When the
   aside is rendered, the layout becomes a 3-column grid (empty spacer | feed |
   stats) so the feed stays in the page's true horizontal center. Without the
   aside, the wrapper falls back to a block layout and .shatter-main centers
   itself with its original max-width: 600px / margin: 0 auto rule. */
.shatter-layout { display: block; }
.shatter-layout-with-aside {
    display: grid;
    grid-template-columns: 240px 600px 240px;
    gap: 24px;
    max-width: 1128px;
    margin: 0 auto;
    align-items: start;
}
.shatter-layout-with-aside .shatter-main {
    max-width: 600px;
    margin: 0;
}
.shatter-layout-spacer { /* intentionally empty — sized by the grid track */ }

.shatter-stats-panel {
    position: sticky;
    top: calc(var(--shatter-header-height) + 12px);
    margin-top: 12px;
    padding: 12px 14px;
    border: 1px solid var(--shatter-border, #2a2a2a);
    border-radius: 10px;
    background: var(--shatter-card-bg, rgba(255,255,255,0.02));
    color: var(--shatter-text, inherit);
    font-size: 13px;
}
.shatter-stats-title {
    margin: 0 0 10px;
    font-size: 14px;
    font-weight: 700;
}
.shatter-stats-window {
    margin: 0 0 12px;
    padding: 0 0 10px;
    border-bottom: 1px solid var(--shatter-border, #2a2a2a);
}
.shatter-stats-window:last-of-type {
    border-bottom: none;
    margin-bottom: 0;
    padding-bottom: 0;
}
.shatter-stats-window-title {
    margin: 0 0 6px;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--shatter-muted, #888);
    font-weight: 700;
}
.shatter-stats-list {
    margin: 0;
    padding: 0;
}
.shatter-stats-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 2px 0;
    font-size: 13px;
}
.shatter-stats-row dt {
    margin: 0;
}
.shatter-stats-row dd {
    margin: 0;
    font-variant-numeric: tabular-nums;
}
.shatter-stats-row-total {
    font-weight: 700;
    border-bottom: 1px dotted var(--shatter-border, #2a2a2a);
    padding-bottom: 4px;
    margin-bottom: 4px;
}
.shatter-stats-updated {
    margin: 10px 0 0;
    opacity: 0.65;
    font-size: 11px;
}

@media (max-width: 900px) {
    .shatter-layout-with-aside {
        grid-template-columns: 1fr;
        max-width: 600px;
    }
    .shatter-layout-spacer { display: none; }
    .shatter-stats-panel { position: static; }
}

/* Multi-actor embed (/shatter/multi) — feed-only. The visible actor swaps in
   response to inbound PostMessages from the parent page; there's no in-iframe
   switcher UI. No banner, no bio, no tabs, no rotator header. */
.shatter-body-multi .shatter-main { padding-top: 0; }
/* Decisive-swap momentum: when applyFilter runs it adds `is-swapping` to the
   feed for one frame; the feed quickly drops to 0.4 opacity then animates back
   to 1, giving the user a visible cue that the swap happened. */
.shatter-body-multi .shatter-feed {
    transition: opacity 120ms ease;
}
.shatter-body-multi .shatter-feed.is-swapping {
    opacity: 0.4;
}
/* The UA [hidden] { display: none } rule has specificity 0,0,1,0 — the same as
   .shatter-card { display: flex }, so author rules win the cascade and the
   "hidden" card stays visible. Combining the data attribute with [hidden]
   bumps specificity to 0,0,2,0, which beats the plain class without needing
   !important. Applies to every multi-mode feed item (cards, thread pairs,
   empty placeholders). */
[data-multi-actor-id][hidden] {
    display: none;
}
