/* ============================================================
   feed.css — post-card and compose-flow surfaces
   ============================================================
   This file owns post-card markup, link previews, mentions,
   shared/quote-post chrome, post menu, post body, compose flow
   (FAB + compose modal + visibility menu + media editor + carousel),
   and the post detail side panel. These styles apply wherever post
   cards or the compose flow appear: the standalone post page, the
   profile feed, drafts page, and the dashboard panel.

   Dashboard-grid-scoped overrides on these same components live in
   dashboard.css (commit 4 sibling) — anything that needs different
   sizing/spacing/behavior inside the dashboard grid is overridden
   there.

   Profile-specific feed features are split: the avatar hover-popover
   lives here (it shows in post author rows everywhere), while the
   pinned-post and free-space surfaces live in profile.css.

   Reference: markdown/design/DESIGN_DECISIONS_CSS_REFACTOR.md
   ============================================================ */

/* ============================================================
   Mentions — @username links inside post and comment text
   ============================================================
   The mention class is applied by render_mentions() in feed/utils.py.
   Style change here flows everywhere @mentions render.
   ============================================================ */

.mention {
    color: var(--color-accent);
    font-weight: var(--font-weight-semibold);
}

/* ============================================================
   Link previews — Open Graph card under posts with URLs
   ============================================================ */

.link-preview {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    overflow: hidden;
    transition: box-shadow var(--transition-medium);
}

.link-preview:hover {
    box-shadow: var(--shadow-md);
}

.link-preview-img {
    max-height: 200px;
    object-fit: cover;
}

.link-preview-title {
    font-size: var(--font-size-body);
    color: var(--color-text);
}

.link-preview-desc {
    font-size: var(--font-size-base);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* ============================================================
   Shared post — embedded card for quote/repost/orphaned share
   ============================================================
   Used as the inset card inside a parent post-card when the post
   has a share_type (quote, repost, or orphaned). Renders as a
   subtle inset surface that's visually distinct from the outer
   card without going full bg-light (which hardcoded #f8f9fa and
   broke in dark mode).
   ============================================================ */

.shared-post-card {
    background-color: var(--color-bg-subtle);
    border-color: var(--color-border-subtle);
}

/* ============================================================
   Unread badge — small dot on the avatar / drafts icon
   ============================================================
   Used in base.html for the avatar pending-follows indicator
   and in the nav for unread messages.
   ============================================================ */

.unread-badge {
    position: absolute;
    top: -2px;
    right: -2px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background-color: var(--color-text-muted);
    border: 2px solid var(--color-text);
}

/* Inline variant — sits beside text inside list rows (e.g. the people
   modal's "follow requests" row) instead of anchoring to a corner. */
.unread-badge-inline {
    position: static;
    display: inline-block;
    margin-left: var(--space-2);
    vertical-align: middle;
}

/* ============================================================
   Post action menu — three-dot dropdown
   ============================================================
   Two trigger conditions for the menu:
     - Bootstrap toggles .show on .dropdown-menu when opened
     - aria-expanded on the trigger button reflects open state
   Animation uses opacity + transform for the smoothest feel
   without fighting Bootstrap's display:none/block toggle.
   ============================================================ */

/* Wrapper positioning — extracted from inline style. */
.post-menu {
    top: var(--space-2);
    right: var(--space-2);
    z-index: 2;
}

/* The trigger button — three vertical dots. Faded by default,
   full opacity on hover/open. */
.post-menu .btn {
    opacity: 0.5;
    border-radius: var(--radius-sm);
    transition: opacity var(--transition-medium),
        background-color var(--transition-fast);
}

.post-menu .btn:hover,
.post-menu .btn[aria-expanded="true"] {
    opacity: 1;
    background-color: var(--color-bg-subtle);
}

/* Focus ring for keyboard users — only when keyboard-focused. */
.post-menu .btn:focus-visible {
    opacity: 1;
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent) 40%, transparent);
}

/* The menu panel itself — frosted glass, animated reveal.
   We force display:block always so transitions work, then use
   visibility + opacity + pointer-events to hide/show. */
.post-menu-list {
    display: block;
    visibility: hidden;
    opacity: 0;
    pointer-events: none;

    /* Visual treatment — frosted glass. */
    font-size: var(--font-size-base);
    border-radius: var(--radius-lg);
    border: 1px solid var(--color-border-subtle);
    padding: 6px;
    min-width: 200px;
    background-color: color-mix(in srgb, var(--color-bg-surface) 80%, transparent);
    backdrop-filter: blur(12px) saturate(1.5);
    -webkit-backdrop-filter: blur(12px) saturate(1.5);
    box-shadow: var(--shadow-lg);

    /* Animation — slight downward translate + scale-from-top-right.
       The transform-origin matters: dropdown-menu-end means the
       menu's top-right corner aligns with the trigger, so scaling
       from top-right makes it grow visually FROM the trigger. */
    transform: translateY(-4px) scale(0.96);
    transform-origin: top right;
    transition: opacity 180ms cubic-bezier(0.32, 0.72, 0, 1),
        transform 180ms cubic-bezier(0.32, 0.72, 0, 1),
        visibility 0s linear 180ms;
}

/* Open state — Bootstrap adds .show when the menu is open. */
.post-menu-list.show {
    visibility: visible;
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0) scale(1);
    /* On open, visibility flips immediately (no delay) so the
       opacity/transform transitions are visible. */
    transition: opacity 180ms cubic-bezier(0.32, 0.72, 0, 1),
        transform 180ms cubic-bezier(0.32, 0.72, 0, 1),
        visibility 0s;
}

/* Dropdown items — refined hover, focus ring, smooth transitions. */
.post-menu-list .dropdown-item {
    border-radius: var(--radius-sm);
    padding: 6px 10px;
    transition: background-color var(--transition-fast),
        color var(--transition-fast);
}

.post-menu-list .dropdown-item:hover,
.post-menu-list .dropdown-item:focus {
    background-color: var(--color-bg-subtle);
    color: var(--color-text);
}

.post-menu-list .dropdown-item:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent) 40%, transparent);
}

.post-menu-list .dropdown-item:active {
    background-color: var(--color-accent);
    color: var(--color-text-on-accent);
}

/* Section label inside the dropdown (e.g. "visibility"). */
.post-menu-section-label {
    font-size: var(--font-size-sm);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--color-text-muted);
    padding: 4px 10px 2px;
}

/* Check icon next to active visibility option — fades in/out. */
.post-menu-list .check-icon {
    opacity: 0;
    transition: opacity var(--transition-fast);
}

.post-menu-list .check-icon.is-active {
    opacity: 1;
}

/* Reduced-motion respect — disable animations for users who've
   asked the OS to minimize motion. */
@media (prefers-reduced-motion: reduce) {

    .post-menu .btn,
    .post-menu-list,
    .post-menu-list .dropdown-item,
    .post-menu-list .check-icon {
        transition: none;
    }

    .post-menu-list,
    .post-menu-list.show {
        transform: none;
    }
}

/* ============================================================
   Per-post flash — auto-fade after a few seconds
   ============================================================
   Animation defined in @keyframes post-flash-fade below.
   The .card .alert selector is broad — auto-fades any alert
   inside any card. If we add a non-flash alert in a card later,
   scope this to .post-flash class. (Logged in TODOs.md.)
   ============================================================ */

.card .alert {
    animation: post-flash-fade 3.5s ease forwards;
}

@keyframes post-flash-fade {
    0% {
        opacity: 0;
        transform: translateY(-4px);
    }

    10% {
        opacity: 1;
        transform: translateY(0);
    }

    85% {
        opacity: 1;
    }

    100% {
        opacity: 0;
        transform: translateY(-4px);
    }
}

/* ============================================================
   Post body & comment body — text wrapping + sizing
   ============================================================ */

.post-body {
    white-space: pre-wrap;
}

.post-author-name {
    font-size: var(--font-size-body);
}

.shared-author-name {
    font-size: var(--font-size-base);
}

.comment-body {
    font-size: var(--font-size-body);
    white-space: pre-wrap;
}

.comment-author-name {
    font-size: var(--font-size-base);
}

.post-image {
    max-width: 100%;
    height: auto;
}

/* ============================================================
   Post video (media1)

   Mirror of .post-image sizing so a video sits in the same content
   slot as an image without layout shift. The wrap div is for the
   transcode-pending overlay positioning -- the <video> sits inside
   it and the badge anchors to the wrap.

   max-height: 70vh prevents portrait phone videos (9:16, often
   1080×1920 native) from rendering at proportional height-auto
   from a 100%-width column, which on most screens comes out around
   1.5x the visible viewport and "expands outside the player."
   With max-height capped + the wrap flex-centered, tall portrait
   videos pillarbox horizontally and stay within a comfortable
   inline-playback size. Landscape videos already fit cleanly.
   ============================================================ */
.post-media-video-wrap {
    position: relative;
    max-width: 100%;
    display: flex;
    justify-content: center;
    background: #000;
    border-radius: var(--radius-md);
    overflow: hidden;
}

.post-video {
    max-width: 100%;
    max-height: 70vh;
    width: auto;
    height: auto;
    display: block;
    background: #000;
    border-radius: var(--radius-md);
}

/* Inside a multi-media Swiper slide the wrap stretches to fill the
   slide's aspect-ratio box (mirrors the .post-image override in the
   carousel section below). object-fit:contain keeps video pillarbox/
   letterbox rather than cropping. */
.media-carousel .swiper-slide .post-media-video-wrap,
.media-carousel .swiper-slide .post-video {
    width: 100%;
    height: 100%;
}
.media-carousel .swiper-slide .post-video {
    object-fit: contain;
}

/* media1 -- pending-transcode overlay. Visible while the RQ worker is
   still converting the video to h264. Positioned top-right, on top of
   the poster, so the user understands "this is processing" rather than
   "this video is broken." Once the worker bumps transcode_status to
   "complete" the template skips this badge and the user sees a normal
   playable video on next page render. */
.post-media-transcode-pending {
    position: absolute;
    top: 8px;
    right: 8px;
    background: rgba(0, 0, 0, 0.78);
    color: var(--color-text-on-accent);
    font-family: var(--font-display, 'VT323'), monospace;
    font-size: 13px;
    letter-spacing: 0.04em;
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--color-border);
    pointer-events: none;
    /* Subtle pulse to draw the eye -- the user just uploaded and
       expects to see something here. Reduced-motion users get a
       static badge via the @media guard below. */
    animation: post-media-transcode-pulse 1.8s ease-in-out infinite;
}

@keyframes post-media-transcode-pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.65; }
}

@media (prefers-reduced-motion: reduce) {
    .post-media-transcode-pending {
        animation: none;
    }
}

/* ============================================================
   Compose form — visibility dropdown + textarea
   ============================================================ */

.compose-visibility-wrap {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 2;
}

.compose-visibility-menu {
    min-width: 8rem;
    font-size: var(--font-size-base);
}

.compose-visibility-menu .dropdown-item {
    cursor: pointer;
}

/* ============================================================
   Edit-post media thumbs — existing media + remove button
   ============================================================ */

.media-thumb-edit {
    position: relative;
    width: 80px;
    height: 80px;
}

.media-thumb-edit img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.remove-media-btn {
    position: absolute;
    /* media1 followup -- moved INSIDE the thumb (was top:-8 right:-8
       perched on the corner). top:4 right:4 puts the button solidly
       on top of the media so it's always over the media frame, never
       floating off into surrounding chrome. Bumped z-index so it
       stacks above any badge or video controls. */
    top: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    background: #1a1a1a;
    color: #ffffff;
    border: 2px solid var(--color-bg-surface);
    border-radius: 50%;
    padding: 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    font-weight: 600;
    line-height: 1;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
    transition: background var(--transition-fast), transform var(--transition-fast);
    z-index: 10;
}

.remove-media-btn:hover,
.remove-media-btn:focus-visible {
    background: var(--color-danger);
    transform: scale(1.1);
}

/* === === ===
   Edit-media button — small pencil/crop icon on each preview thumb.
   Sibling of .remove-media-btn (positioned in the opposite corner).
   Only visible when window.openMediaEditor exists at render time.
   === === === */
.edit-media-btn {
    position: absolute;
    /* video-editor PR-C followup -- moved from top:4 left:4 to
       top:4 right:4. The old top-left position sat over the letter-
       box space when the contained media was narrower than the slide
       (portrait clip in a landscape slide etc.), which made the
       button look "cut off" and disconnected from the media. Top-
       right mirrors the delete icon's corner so action buttons read
       as a family, and the trim/mute buttons (which inherit this
       class for positioning) already live on the right side too. */
    top: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;

    background: #1a1a1a;
    color: #ffffff;
    border: 2px solid var(--color-bg-surface);
    border-radius: 50%;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);

    cursor: pointer;
    transition: background var(--transition-fast),
        transform var(--transition-fast);
    z-index: 10;
}

.edit-media-btn:hover,
.edit-media-btn:focus-visible {
    background: var(--color-accent);
    transform: scale(1.1);
}

/* In the simple-preview thumb (compose-form path) the slide already
   has its own .remove-media-btn at top:4 right:4. Stack the edit
   pencil vertically below the delete so they don't overlap. The
   edit-carousel slide path doesn't need this -- the carousel-level
   delete (.remove-active-media-btn) sits on the carousel container,
   not the slide, so the slide's top-right corner is empty. */
.media-thumb-edit .edit-media-btn {
    top: 36px;
}

/* media1 followup — trim button on video preview thumbs. Bottom-right
   corner INSIDE the thumb so it's always over the media. Matches
   .remove-media-btn / .edit-media-btn placement convention. */
.trim-media-btn {
    position: absolute;
    bottom: 4px;
    right: 4px;
    width: 28px;
    height: 28px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: #1a1a1a;
    color: #ffffff;
    border: 2px solid var(--color-bg-surface);
    border-radius: 50%;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
    cursor: pointer;
    font-size: 16px;
    line-height: 1;
    transition: background var(--transition-fast),
        transform var(--transition-fast);
    z-index: 10;
}

.trim-media-btn:hover,
.trim-media-btn:focus-visible {
    background: var(--color-accent);
    transform: scale(1.1);
}

/* media1 followup — sizing for the <video> element inside the small
   preview-thumb wrap. The default <video> width is 300px which would
   overflow the 80px square. object-fit:cover on the video matches the
   image's behavior so video previews look like image previews. */
.media-thumb-edit video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    background: #000;
}

/* media1 followup (video-ux-parity) -- shared play-button badge for
   video poster thumbs. Originally lived as .media-column-tile-video-
   badge in dashboard.css scoped to the dashboard media column tile;
   lifted here under a more generic name + selector so it can be
   applied uniformly across all surfaces that render a video's poster
   frame as an <img> (messaging threads, share-post previews, reshare
   confirmations, linked-event thumb strips, the edit-mode carousel,
   and the dashboard media column itself).

   Anchors to the nearest position:relative ancestor -- callers wrap
   the <img> in a div with position-relative + d-inline-block (or use
   a containing block that's already positioned) so the badge centers
   over the poster.

   Pointer-events:none so the badge doesn't intercept clicks meant
   for the underlying <img> (zoom modal, anchor wrappers, etc.). */
.media-thumb-video-badge {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 48px;
    height: 48px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.55);
    color: var(--color-text-on-accent);
    font-size: 20px;
    border-radius: 50%;
    pointer-events: none;
    /* Slight nudge for the unicode play glyph -- the character has
       baked-in left padding that pulls it visually left of center. */
    padding-left: 4px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
}

/* Smaller variant for tight surfaces (the 64px linked-event thumbs,
   compose previews). Half-size badge so it doesn't dwarf the thumb. */
.media-thumb-video-badge--sm {
    width: 24px;
    height: 24px;
    font-size: 11px;
    padding-left: 2px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

/* media1 followup -- trim pill. Renders on a video preview thumb
   when the user has confirmed a trim window. Purely informational:
   the actual trim happens server-side via native ffmpeg before the
   file is uploaded to B2. Pill text is "0:05 → 0:25" (formatted by
   formatTrimLabel in media_preview.js).

   Pinned to the bottom-left so it doesn't collide with the action
   buttons (remove top-right, trim bottom-right). Same dark
   translucent backdrop pattern the action buttons use so the pill
   reads on any underlying frame. */
.media-thumb-trim-pill {
    position: absolute;
    bottom: 4px;
    left: 4px;
    background: rgba(10, 14, 26, 0.92);
    color: #ffffff;
    font-family: var(--font-display, 'VT323'), monospace;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    padding: 3px 8px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--color-accent);
    pointer-events: none;
    z-index: 5;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}

/* On a carousel slide the pill can grow slightly. */
.swiper-slide .media-thumb-trim-pill {
    font-size: 13px;
    padding: 4px 10px;
}

/* video-editor PR-C -- quick-mute button on video preview thumbs.
   Visual sibling of .trim-media-btn (bottom-right corner) -- we put
   the mute button bottom-right of the trim button so the two action
   buttons stack vertically. Same dark-on-light + accent-on-hover
   pattern as the other action buttons for visual family. The .active
   state inverts to accent-on-dark so the muted state reads
   unambiguously even without the audio badge. */
.mute-media-btn {
    position: absolute;
    bottom: 36px; /* sits above .trim-media-btn (bottom: 4 + 28 + 4 gap) */
    right: 4px;
    width: 28px;
    height: 28px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: #1a1a1a;
    color: #ffffff;
    border: 2px solid var(--color-bg-surface);
    border-radius: 50%;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
    cursor: pointer;
    transition: background var(--transition-fast),
        transform var(--transition-fast);
    z-index: 10;
}

.mute-media-btn:hover:not(:disabled),
.mute-media-btn:focus-visible:not(:disabled) {
    background: var(--color-accent);
    transform: scale(1.1);
}

/* Active = currently flagged for mute (compose-time toggled on) OR
   already-muted existing media. The accent color signals "edit
   pending" / "edit applied"; combined with the audio badge over the
   thumb the user has two redundant cues that audio is gone. */
.mute-media-btn.active {
    background: var(--color-accent);
    color: var(--color-text-on-accent, #1a1a1a);
}

.mute-media-btn:disabled {
    cursor: not-allowed;
    opacity: 0.65;
    background: var(--color-accent);
    color: var(--color-text-on-accent, #1a1a1a);
}

/* video-editor PR-C -- audio-removed badge. Overlays the bottom-
   right of the thumb (where the play badge is centered, so they
   don't collide; play stays centered, audio badge anchors corner).
   Same dark-translucent backdrop as the trim pill so it reads on any
   underlying frame. Pointer-events:none so clicks fall through to
   the zoom modal / link wrapper underneath.

   Renders across all surfaces that show video media: compose
   previews, edit carousel, viewer carousel, dashboard tile,
   messaging thread, share-post preview, linked-event thumbs. The
   shared class lets the badge appear consistently wherever .media-
   thumb-video-badge appears today. */
.media-thumb-audio-badge {
    position: absolute;
    bottom: 4px;
    right: 4px;
    width: 24px;
    height: 24px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: rgba(10, 14, 26, 0.85);
    color: #ffffff;
    border-radius: 50%;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
    pointer-events: none;
    z-index: 6;
}

/* Tighter variant for the 64px linked-event thumbs etc. -- mirror of
   .media-thumb-video-badge--sm so the two badge variants stay in
   visual proportion. */
.media-thumb-audio-badge--sm {
    width: 16px;
    height: 16px;
}

.media-thumb-audio-badge--sm svg {
    width: 10px;
    height: 10px;
}

/* video-editor PR-D followup -- expand-to-fullscreen button on
   compose-time video thumbs. Top-left corner so it sits opposite
   the delete button at top-right and doesn't collide with the
   stacked action column (edit-pencil, mute, trim) on the right
   side. The icon is the universal "expand arrows" glyph from
   Feather. Same dark-circle visual family as the other action
   buttons. */
.expand-media-btn {
    position: absolute;
    top: 4px;
    left: 4px;
    width: 28px;
    height: 28px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: #1a1a1a;
    color: #ffffff;
    border: 2px solid var(--color-bg-surface);
    border-radius: 50%;
    box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
    cursor: pointer;
    transition: background var(--transition-fast),
        transform var(--transition-fast);
    z-index: 10;
}

.expand-media-btn:hover,
.expand-media-btn:focus-visible {
    background: var(--color-accent);
    transform: scale(1.1);
}

/* video-editor PR-D -- crop tab styling. Lives inside the multi-
   tab edit modal. Stage container constrains the cropper canvas to
   the same vertical envelope as the trim tab's video player so the
   modal height doesn't snap around on tab switches. Aspect-ratio
   chips reuse Bootstrap's btn-outline-secondary base with a custom
   .active state that mirrors the image editor's media-editor-aspect
   pattern (filled accent background). */
.video-edit-tabs .nav-link {
    color: var(--color-text);
}

.video-edit-tabs .nav-link.active {
    color: var(--color-accent);
    background-color: transparent;
    border-bottom-color: var(--color-accent);
}

.video-edit-crop-stage {
    position: relative;
    width: 100%;
    /* Match the trim tab's video player envelope so tab switches
       don't shift the modal-body height. The trim player caps at
       60vh; crop stage matches that ceiling and adds a sensible
       min-height so the empty-state hint has room. */
    min-height: 280px;
    max-height: 60vh;
    background: #000;
    border-radius: var(--radius-md);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.video-edit-crop-empty {
    color: var(--color-text-muted, #888);
    font-size: 0.9rem;
    text-align: center;
    padding: var(--space-3);
}

.video-edit-crop-image {
    max-width: 100%;
    max-height: 60vh;
    display: block;
}

.video-edit-aspect-btn.active {
    background-color: var(--color-accent);
    color: var(--color-text-on-accent, #1a1a1a);
    border-color: var(--color-accent);
}

.video-edit-aspect-btn.active:hover,
.video-edit-aspect-btn.active:focus {
    background-color: var(--color-accent);
    color: var(--color-text-on-accent, #1a1a1a);
    border-color: var(--color-accent);
}

/* video-editor PR-F -- filter tab. Preview area mirrors the crop
   stage envelope so tab switches don't snap modal height. Strip
   wraps so 8 thumbnails fit on narrow viewports without horizontal
   scroll. */
.video-edit-filter-preview {
    position: relative;
    width: 100%;
    min-height: 200px;
    max-height: 50vh;
    background: #000;
    border-radius: var(--radius-md);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}

.video-edit-filter-empty {
    color: var(--color-text-muted, #888);
    font-size: 0.9rem;
    text-align: center;
    padding: var(--space-3);
}

.video-edit-filter-preview-img {
    max-width: 100%;
    max-height: 50vh;
    display: block;
    /* CSS filter is updated by JS on preset click for live preview. */
    transition: filter 120ms ease-out;
}

.video-edit-filter-strip {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin-top: var(--space-3);
}

/* video-editor PR-E -- slide-in edit panel. REPLACES the PR-B/C/D
   trim modal. The edit surface is now a fixed-position div that
   slides in from the right via translateX(100%) -> translateX(0)
   so there's no Bootstrap modal in play here at all -- the only
   Bootstrap modal in the stack (if any) is the compose modal that
   sits underneath, which avoids the modal-on-modal backdrop /
   focus-trap / body-scroll-lock confusion that PR-B/C/D's modal
   ran into. Backdrop is a sibling element so its opacity transition
   can run independently of the panel's transform. z-index 1085
   sits above any Bootstrap modal (1055/1075 for image editor) and
   below toasts (1090). */
.video-edit-panel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: min(720px, 100vw);
    max-width: 100vw;
    background: var(--color-bg-surface, #fff);
    box-shadow: -8px 0 24px rgba(0, 0, 0, 0.25);
    z-index: 1085;
    transform: translateX(100%);
    transition: transform 280ms cubic-bezier(0.32, 0.72, 0, 1);
    will-change: transform;
    /* Hidden from layout when not open so it doesn't catch clicks
       through invisible chrome. The .is-open state restores
       visibility + interactivity. */
    visibility: hidden;
    display: flex;
    flex-direction: column;
}

.video-edit-panel.is-open {
    transform: translateX(0);
    visibility: visible;
}

.video-edit-panel-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 1084;
    opacity: 0;
    transition: opacity 280ms ease-out;
    pointer-events: none;
    visibility: hidden;
}

.video-edit-panel-backdrop.is-open {
    opacity: 1;
    pointer-events: auto;
    visibility: visible;
}

/* Wrapping container for the panel's interior chrome. matrix-
   dissolve targets this on save so the particle effect runs on
   the entire visible surface, mirroring how the modal version
   ran on .modal-content. */
.video-edit-panel-content {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}

.video-edit-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    /* mobile-audit followup: same regression shape as the media-zoom
       close button — panel is position:fixed top:0, so the header's
       close button sits ~12px from viewport top and gets covered by
       the iOS Dynamic Island. max() keeps non-iOS at the design value. */
    padding: max(var(--space-3), var(--safe-area-top)) var(--space-3) var(--space-3);
    border-bottom: 1px solid var(--color-border, rgba(0, 0, 0, 0.1));
    flex-shrink: 0;
}

.video-edit-panel-title {
    margin: 0;
    font-size: 1.125rem;
    font-weight: 600;
}

.video-edit-panel-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: var(--space-3);
}

.video-edit-panel-footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-2);
    padding: var(--space-3);
    border-top: 1px solid var(--color-border, rgba(0, 0, 0, 0.1));
    flex-shrink: 0;
}

/* Body scroll lock when the panel is open. Mirrors what Bootstrap
   does for its own modals via .modal-open on body. */
body.video-edit-panel-open {
    overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
    .video-edit-panel {
        transition: none;
    }
    .video-edit-panel-backdrop {
        transition: none;
    }
}

/* media1 followup — trim modal styling. The native <video> player
   inside the modal needs a max-width so portrait clips don't blow
   out the modal-lg width. Background is solid black so letterboxing
   reads as intentional, not broken. */
.video-trim-player-wrap {
    display: flex;
    justify-content: center;
    background: #000;
    border-radius: var(--radius-md);
    overflow: hidden;
}

.video-trim-player {
    max-width: 100%;
    max-height: 60vh;
    width: auto;
    height: auto;
    display: block;
}

/* iOS-style scrubber. Wraps the thumbstrip + nouislider overlay so
   they stack at the same X coordinates -- the slider sits absolutely
   positioned on top of the thumbnail row. Height = thumb height +
   a few px of slider chrome above/below; tweak both together. The
   yellow accent color matches iOS Photos' trim chrome (and our brand
   yellow), giving the selected range the unmistakable "trimming"
   affordance users already pattern-match on. */
.video-trim-scrubber {
    position: relative;
    padding: 12px 0;
}

.video-trim-thumbstrip {
    display: flex;
    gap: 2px;
    width: 100%;
    height: 54px;
    background: var(--color-bg-subtle, #1a1a1a);
    border-radius: 6px;
    overflow: hidden;
    pointer-events: none;
    /* Inset shadow gives the strip the same "recessed well" look
       iOS uses, reinforcing that the slider sits on top of it. */
    box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}

.video-trim-thumb,
.video-trim-thumb-placeholder {
    flex: 1 1 0;
    min-width: 0;
    height: 100%;
    background: var(--color-bg-muted, #2a2a2a);
    overflow: hidden;
}

.video-trim-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

/* nouislider overlay. Sits on top of the thumbstrip; transparent
   track so the thumbnails show through. The connect bar between
   the two handles is the brand yellow at 35% opacity -- visible
   enough to read "this is selected" but translucent enough to see
   which frames are inside the trim window. */
.video-trim-slider.noUi-target {
    position: absolute;
    top: 6px;
    left: 0;
    right: 0;
    height: 66px;
    background: transparent;
    border: 0;
    box-shadow: none;
    border-radius: 6px;
}

.video-trim-slider.noUi-horizontal .noUi-connect {
    background: rgba(255, 199, 0, 0.35);
    border-top: 2px solid var(--color-accent, #ffc700);
    border-bottom: 2px solid var(--color-accent, #ffc700);
    box-shadow: none;
}

.video-trim-slider.noUi-horizontal .noUi-handle {
    width: 14px;
    height: 78px;
    top: -6px;
    right: -7px;
    background: var(--color-accent, #ffc700);
    border: 1px solid rgba(0, 0, 0, 0.2);
    border-radius: 3px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
    cursor: ew-resize;
}

/* Hide nouislider's default grip pseudo-elements -- the iOS handle
   is a clean colored bar with no internal lines. */
.video-trim-slider.noUi-handle::before,
.video-trim-slider.noUi-handle::after {
    display: none;
}

/* Untrimmed regions outside the connect bar: dim the thumbnails
   underneath so the selected range pops. nouislider doesn't render
   anything for the unconnected portions, but we can fake the dimming
   with two ::before/::after gradients on the slider container. We
   skip that complexity for now -- the connect-bar contrast alone
   reads clearly enough. */

.video-trim-loop-btn {
    text-decoration: none;
    font-size: 0.875rem;
    padding: 0;
}

.video-trim-loop-btn:hover {
    text-decoration: underline;
}

/* ============================================================
   Media editor (me1)
   Cropper.js modal — crop, rotate, aspect ratio chips.
   ============================================================ */

/* Modal-on-modal: when the editor opens over the compose modal,
   Bootstrap's default z-index puts both at 1055. Bump the editor
   so it sits cleanly above the compose modal and its backdrop. */
#mediaEditorModal {
    z-index: 1075;
}

#mediaEditorModal+.modal-backdrop,
.modal-backdrop.show~#mediaEditorModal+.modal-backdrop {
    z-index: 1070;
}

/* video-editor PR-E -- the trim modal z-index bump from PR-D
   followup is gone because the trim/edit surface is no longer a
   Bootstrap modal. See the .video-edit-panel block earlier in this
   file for the panel's own z-index 1085 (above any Bootstrap
   modal). #mediaEditorModal's 1075 above stays since the image
   editor is still a true modal. */

.media-editor-dialog {
    max-width: min(720px, 92vw);
}

.media-editor-canvas {
    /* Constrain the cropper stage to a comfortable viewing size.
       Cropper.js fills the container; the container drives the size. */
    max-height: 60vh;
    min-height: 320px;
    width: 100%;
    overflow: hidden;
    background-color: var(--color-bg-subtle);
    border-radius: var(--radius-md, 0.5rem);
}

.media-editor-canvas img {
    /* Cropper.js sizes its UI to match the source image's rendered
       dimensions at init time. Force the image to fill the canvas
       container *before* Cropper mounts so it builds a properly-sized
       UI. object-fit: contain preserves aspect ratio — a portrait
       photo won't stretch into the landscape container shape. */
    display: block;
    width: 100%;
    height: 100%;
    max-width: 100%;
    object-fit: contain;
}

/* ============================================================
   Media editor — mobile polish (me5)
   ============================================================
   modal-dialog-scrollable on the dialog gives us pinned header
   and footer with a scrolling body — load-bearing on small phones
   where the canvas + filter strip + controls + footer otherwise
   overflow the viewport.

   The filter strip already scrolls horizontally; the fade gradient
   below signals there's more to scroll to. Without it, on narrow
   screens the rightmost visible thumb just looks like the last
   one.
   ============================================================ */

/* Right-edge fade on the filter strip — only when the content
   actually overflows. background-image is layered on top of the
   strip's existing background-color, anchored to the right
   container edge so it stays visible regardless of scroll
   position. Pointer-events none so it doesn't intercept clicks
   on the underlying thumbs. */
.media-editor-filter-strip {
    position: relative;
    /* Mask the right edge with a subtle gradient so users see
       there's more to scroll to. CSS mask-image is wider-supported
       in 2026 than the old vendor-prefixed version was; this is
       still gracefully degraded — older browsers just don't see
       the fade. */
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
    mask-image: linear-gradient(to right, black calc(100% - 24px), transparent);
}

/* Lasagna hint — small disclaimer below the filter strip. */
.media-editor-filter-hint {
    color: var(--color-text-muted);
    font-size: var(--font-size-sm);
    font-style: italic;
}

/* The canvas needs a sensible min-height even on very short
   viewports. 60vh on a 568px-tall phone is 340px which is fine,
   but rotating to landscape (where vh becomes ~375px) gives only
   225px, which feels cramped. Floor it at 240px and let scroll
   handle the rest. */
@media (max-height: 600px) {
    .media-editor-canvas {
        min-height: 240px;
        max-height: 50vh;
    }
}

/* On narrow screens the aspect-ratio chip row + rotate buttons
   already stack via the existing @media(max-width: 540px) rule.
   Make sure the chips don't grow weird when stacked. */
@media (max-width: 540px) {
    .media-editor-aspect-group {
        width: 100%;
        justify-content: center;
    }

    .media-editor-rotate-group {
        width: 100%;
        justify-content: center;
    }
}

/* ============================================================
   Media editor — filter strip (me2)
   Horizontal scrollable row of preview thumbs, each showing the
   source image with a different CSS filter preset baked into a
   small offscreen canvas. Active state mirrors the aspect-ratio
   chip active state — violet ring + label tint.
   ============================================================ */

.media-editor-filter-strip {
    display: flex;
    gap: var(--space-2);
    overflow-x: auto;
    padding-bottom: var(--space-1);
    /* breathing room for scrollbar */

    /* Momentum scrolling on iOS — important since the modal is the
       primary editing surface on phones. */
    -webkit-overflow-scrolling: touch;

    /* Hide the horizontal scrollbar on browsers that respect this.
       Strip is meant to feel like a chip row, not a scroll region. */
    scrollbar-width: thin;
}

.media-filter-thumb {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 0;
    background: none;
    border: 2px solid transparent;
    border-radius: var(--radius-md, 0.5rem);
    cursor: pointer;
    transition: border-color var(--transition-fast),
        transform var(--transition-fast);
}

.media-filter-thumb canvas {
    display: block;
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: calc(var(--radius-md, 0.5rem) - 2px);
    background: var(--color-bg-subtle);
}

.media-filter-thumb-label {
    font-size: 11px;
    color: var(--color-text-muted);
    text-transform: lowercase;
    white-space: nowrap;
}

.media-filter-thumb:hover,
.media-filter-thumb:focus-visible {
    border-color: color-mix(in srgb, var(--color-accent) 50%, transparent);
}

.media-filter-thumb:hover .media-filter-thumb-label,
.media-filter-thumb:focus-visible .media-filter-thumb-label {
    color: var(--color-text);
}

.media-filter-thumb.is-active {
    border-color: var(--color-accent);
}

.media-filter-thumb.is-active .media-filter-thumb-label {
    color: var(--color-accent);
    font-weight: var(--font-weight-semibold);
}

.media-editor-controls {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}

.media-editor-aspect-group .btn.active {
    /* Cropper aspect chip — active state matches the project's
       accent treatment. */
    background-color: var(--color-accent);
    color: white;
    border-color: var(--color-accent);
}

/* On narrow viewports the controls row stacks vertically. */
@media (max-width: 540px) {
    .media-editor-controls {
        flex-direction: column;
        align-items: stretch;
    }

    .media-editor-rotate-group {
        margin-left: 0 !important;
        justify-content: center;
    }
}

/* ============================================================
   Post-card flash banner — small in-card notification
   ============================================================ */

.post-flash {
    font-size: var(--font-size-md);
}

.post-flash .btn-close {
    font-size: var(--font-size-xs);
    padding: 6px;
}

/* === FAB — floating action button (Phase 11b db5) ================ */

/* Base FAB: circular, fixed-position, elevated. The .fab-compose
   modifier picks the actual bottom/right offsets and accent color.

   Sized at 56px (Material Design's standard FAB size — the universal
   "pleasant tap target on mobile, visible-but-not-shouty on desktop"
   default).

   z-index: 1040 sits above SwiperJS's pagination dots (z-index: 10)
   but below Bootstrap's modal backdrop (1050) and modal (1055), so
   when the modal opens the FAB is correctly hidden behind the
   backdrop. */
.fab {
    position: fixed;
    width: 56px;
    height: 56px;
    border-radius: var(--radius-pill);
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: var(--shadow-lg);
    z-index: 1040;
    transition: transform var(--transition-fast),
        box-shadow var(--transition-medium),
        background var(--transition-fast);
}

.fab:hover {
    transform: translateY(-2px);
    box-shadow: 0 12px 28px rgba(0, 0, 0, 0.16);
}

.fab:active {
    transform: translateY(0);
    box-shadow: var(--shadow-md);
}

.fab:focus-visible {
    outline: 3px solid var(--color-accent);
    outline-offset: 3px;
}

/* The compose-specific FAB. Bottom-right positioning, accent color.
   --safe-area-* vars let us sit clear of iOS's home indicator and
   notch on phones; the vars fall back to 0px on desktop. */
.fab-compose {
    bottom: calc(1.5rem + var(--safe-area-bottom));
    right: calc(1.5rem + var(--safe-area-right));
    background: var(--color-accent);
    color: var(--color-text-on-accent);
}

.fab-compose:hover {
    background: var(--color-accent-hover);
}

/* === Compose modal (Phase 11b db5) =============================== */

/* Mobile keyboard handling. Bootstrap's default modal uses 100vh,
   which on iOS doesn't shrink when the keyboard appears — the textarea
   ends up hidden behind it. 100dvh (dynamic viewport height) is the
   visible area minus on-screen UI like the keyboard, so the modal
   fits what the user can actually see.

   Only applies inside .modal-dialog-scrollable; non-scrollable modals
   keep Bootstrap's default sizing. */
#composeModal .modal-dialog-scrollable {
    max-height: calc(100dvh - 1rem);
}

/* Tighter padding on mobile so the form has room to breathe inside
   the smaller modal. */
@media (max-width: 600px) {
    #composeModal .modal-dialog {
        margin: 0.5rem;
    }

    #composeModal .modal-body {
        padding: 0.75rem;
    }

    /* Mobile-only spacing polish (bugfix/nav-overflow-and-narrow).
       Two surfaces that read cramped on phones:
         1. Header: title sits flush against the close button with no
            real flex-gap. Adding `gap` gives the title room to breathe.
            Slight horizontal padding bump for proportionality.
         2. Action row: mt-2 pt-2 in the markup is 8+8=16px above the
            publish row, which collides visually with the textarea on
            narrow screens. Doubles to 16+16=32px via the
            compose-action-row hook. #composeModal prefix bumps the
            specificity over Bootstrap's mt-2/pt-2 utility classes. */
    #composeModal .modal-header {
        padding: 1rem 1.25rem;
        gap: var(--space-2);
    }

    #composeModal .compose-action-row {
        margin-top: var(--space-4);
        padding-top: var(--space-4);
    }
}

/* === Post detail panel (Phase 11b db6) ===========================

   Panel that slides in over the dashboard when a user clicks a post.
   Singleton — there's exactly one panel element in the DOM on the
   dashboard, hidden by default, populated and shown by
   dashboard_panel.js (Phase D) or by server-side rendering when the
   dashboard is hit with ?post=<id> (Phase B).

   Behavior split:
     - Desktop (>= 769px): slides in from the right edge, takes about
       half the viewport width but caps at 540px. Dashboard remains
       partially visible underneath.
     - Mobile (<= 768px): full-viewport takeover, slides up from the
       bottom. Dashboard hidden under the backdrop.

   The .is-open class is the on/off switch — JS toggles it; server
   renders it when ?post=<id> hits the dashboard route.

   Z-index choices:
     - .post-detail-panel-backdrop: 1040 (just below Bootstrap modals
       at 1050, just above standard dropdowns at 1000)
     - .post-detail-panel: 1041 (above its own backdrop)
   ============================================================ */

.post-detail-panel {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    max-width: 540px;
    background-color: var(--bs-body-bg);
    border-left: 1px solid var(--bs-border-color);
    box-shadow: -4px 0 16px rgba(0, 0, 0, 0.08);
    z-index: 1041;
    display: flex;
    flex-direction: column;

    /* Hidden by default: pushed off the right edge. Transition makes
       the slide-in/out animate when .is-open is toggled. */
    transform: translateX(100%);
    transition: transform 280ms ease-out;

    /* When hidden, also remove from accessibility tree so screen
       readers don't see the empty panel. visibility (not display)
       so the transition can play out before fully hiding. */
    visibility: hidden;
}

.post-detail-panel.is-open {
    transform: translateX(0);
    visibility: visible;
}

.post-detail-panel-header {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding: 12px 16px;
    border-bottom: 1px solid var(--bs-border-color);
    /* Sticky in case the body scrolls — header stays put while
       comments scroll under it. */
    position: sticky;
    top: 0;
    background-color: var(--bs-body-bg);
    z-index: 1;
    flex-shrink: 0;
}

.post-detail-panel-close {
    background: transparent;
    border: 0;
    font-size: 20px;
    line-height: 1;
    color: var(--bs-secondary-color);
    cursor: pointer;
    padding: 4px 8px;
    border-radius: 4px;
    transition: background-color 120ms ease;
}

.post-detail-panel-close:hover,
.post-detail-panel-close:focus-visible {
    background-color: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
    outline: none;
}

.post-detail-panel-body {
    flex: 1;
    overflow-y: auto;
    padding: 16px;
    /* Body scroll lock for the panel — prevents the dashboard
       from scrolling underneath when the user reaches the panel's
       scroll boundary. (overscroll-behavior is well-supported by
       2026; no fallback needed.) */
    overscroll-behavior: contain;
}

/* Constrain images inside the panel — without this, large images
   (per the Phase A test screenshot) blow out the panel width. The
   global .post-image rule already does this, but scoping a panel-
   specific rule with higher specificity is forgiveness for any
   inline <img> that doesn't pick up .post-image. */
.post-detail-panel-body img {
    max-width: 100%;
    height: auto;
}

/* === Backdrop ============================================
   Dims the rest of the viewport when the panel is open.

   Desktop: NOT shown. The dashboard remains visible to the
   left of the panel and clicks on it should reach the
   dashboard (so replace-on-second-click works). A backdrop
   covering the dashboard would intercept those clicks.

   Mobile: shown. The panel is full-screen so the backdrop
   only flashes briefly during the slide-in transition, but
   it provides a clean fade behind the panel.
   ======================================================== */

.post-detail-panel-backdrop {
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.4);
    z-index: 1040;
    opacity: 0;
    visibility: hidden;
    transition: opacity 280ms ease-out, visibility 280ms ease-out;
    /* Hidden on desktop — only the mobile media query reveals it. */
    display: none;
}

@media (max-width: 768px) {
    .post-detail-panel-backdrop {
        display: block;
    }

    .post-detail-panel-backdrop.is-open {
        opacity: 1;
        visibility: visible;
    }
}

/* === Mobile takeover ====================================
   Below the standard mobile breakpoint, the panel becomes a
   full-viewport takeover that slides up from the bottom
   instead of in from the right. Per DESIGN_DECISIONS.md §5.
   ======================================================== */

@media (max-width: 768px) {
    .post-detail-panel {
        max-width: none;
        width: 100%;
        height: 100dvh;
        border-left: 0;
        border-top: 1px solid var(--bs-border-color);

        /* Slide UP from the bottom on mobile, not in from the right.
           Override the desktop transform. */
        transform: translateY(100%);
    }

    .post-detail-panel.is-open {
        transform: translateY(0);
    }

    .post-detail-panel-header {
        /* Mobile: account for iOS safe-area inset at the top so the
           close button isn't tucked under the notch / status bar. */
        padding-top: max(12px, var(--safe-area-top));
    }

    .post-detail-panel-body {
        /* Mobile: account for iOS home indicator at the bottom. */
        padding-bottom: max(16px, var(--safe-area-bottom));
    }
}

/* === Body scroll lock when panel is open ================
   When the panel is open, prevent the underlying document
   from scrolling. The panel body has its own scroll. JS in
   Phase D toggles this class on <body>.
   ======================================================== */

body.panel-open {
    overflow: hidden;
}

/* Cursor pointer on dashboard post cards — hint that they're clickable
   to open the panel. Doesn't apply elsewhere (e.g. on the standalone
   post detail page) because .panel-clickable is only on cards
   rendered inside the dashboard's columns. */
.dashboard-grid .panel-clickable {
    cursor: pointer;
}

/* Subtle hover lift on dashboard post cards. Optional polish. */
.dashboard-grid .panel-clickable:hover {
    background-color: var(--bs-tertiary-bg);
}

/* ============================================================
   Post media carousel — multi-image posts (pr11)
   ============================================================
   SwiperJS-driven horizontal carousel for posts with 2+ images.
   Single-image posts skip this entirely — they render as a plain
   <img class="post-image"> outside any carousel container.

   Markup contract (set in _post_card.html / post_detail.html):
     .media-carousel.swiper
       └── .swiper-wrapper
             └── .swiper-slide × N (each containing an <img>)
       └── .media-carousel-pagination.swiper-pagination
       └── .media-carousel-prev (button)
       └── .media-carousel-next (button)
   ============================================================ */

/* Container — establishes the box the slides live inside.
   Aspect ratio caps the height so very tall portrait images
   don't blow out the card. 4:3 felt like a good middle ground
   between landscape-friendly (16:9 too letterboxy for portrait)
   and portrait-friendly (1:1 wastes space for landscape). */
.media-carousel {
    position: relative;
    width: 100%;
    aspect-ratio: 4 / 3;
    max-height: 600px;
    overflow: hidden;
    border-radius: var(--radius-md);
    background-color: var(--color-bg-subtle);
}

/* Each slide fills the carousel; the image inside is constrained
   by the slide. object-fit: contain keeps the whole image
   visible (no cropping), with letterboxing if needed against the
   container's background-color. */
.media-carousel .swiper-slide {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
}

.media-carousel .swiper-slide img.post-image {
    width: 100%;
    height: 100%;
    object-fit: contain;
    /* Override the global .post-image max-width:100% rule's
       implicit height:auto so the image actually fills the slide. */
}

/* === Pagination (dot indicator) ============================
   SwiperJS renders bullets as <span> inside this container.
   We override its defaults to use our accent token. */

.media-carousel .media-carousel-pagination.swiper-pagination {
    position: absolute;
    bottom: var(--space-2);
    left: 0;
    right: 0;
    text-align: center;
    z-index: 2;
    /* Don't intercept clicks on the slide itself. */
    pointer-events: none;
}

.media-carousel .swiper-pagination-bullet {
    display: inline-block;
    width: 7px;
    height: 7px;
    margin: 0 3px;
    border-radius: 50%;
    background-color: var(--color-bg-surface);
    opacity: 0.7;
    box-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
    transition: background-color var(--transition-fast),
        opacity var(--transition-fast),
        transform var(--transition-fast);
    /* Re-enable clicks on bullets specifically — the parent
       has pointer-events: none so the carousel image stays
       clickable everywhere except the dots themselves. */
    pointer-events: auto;
    cursor: pointer;
    border: 0;
}

.media-carousel .swiper-pagination-bullet-active {
    background-color: var(--color-accent);
    opacity: 1;
    transform: scale(1.15);
}

/* === Arrow buttons =========================================
   Hidden by default; appear on hover (desktop) or focus
   (keyboard). Hidden entirely on touch devices — swiping is
   the primary interaction. */

.media-carousel-prev,
.media-carousel-next {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background-color: color-mix(in srgb, var(--color-bg-surface) 80%, transparent);
    color: var(--color-text);
    border: 0;
    font-size: 22px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 3;
    opacity: 0;
    transition: opacity var(--transition-fast),
        background-color var(--transition-fast);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
}

.media-carousel-prev {
    left: var(--space-2);
}

.media-carousel-next {
    right: var(--space-2);
}

/* Reveal on hover (mouse) or focus-within (keyboard). */
.media-carousel:hover .media-carousel-prev,
.media-carousel:hover .media-carousel-next,
.media-carousel:focus-within .media-carousel-prev,
.media-carousel:focus-within .media-carousel-next {
    opacity: 1;
}

.media-carousel-prev:hover,
.media-carousel-next:hover {
    background-color: var(--color-bg-surface);
}

/* Focus ring — matches the rest of the app. */
.media-carousel-prev:focus-visible,
.media-carousel-next:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent) 40%, transparent);
}

/* SwiperJS adds .swiper-button-disabled when at first/last slide
   (since loop: false). Dim those instead of hiding — the user
   still sees the affordance but knows it won't do anything. */
.media-carousel-prev.swiper-button-disabled,
.media-carousel-next.swiper-button-disabled {
    opacity: 0.3 !important;
    cursor: default;
}

/* On touch devices, hide arrows entirely — swipe is primary.
   hover: none catches touch devices reliably. */
@media (hover: none) {

    .media-carousel-prev,
    .media-carousel-next {
        display: none;
    }
}

/* Reduced motion — disable all transitions. SwiperJS's own
   speed: 0 (set in JS) handles slide transitions; this catches
   our hover/dot animations. */
@media (prefers-reduced-motion: reduce) {

    .media-carousel-prev,
    .media-carousel-next,
    .media-carousel .swiper-pagination-bullet {
        transition: none;
    }
}

/* ============================================================
   Edit carousel — remove-active-slide button
   ============================================================
   Pinned top-right of the carousel container, applies to whichever
   slide is currently active. Distinct from .remove-media-btn (which
   was per-slide); this button is per-carousel.
   ============================================================ */
.remove-active-media-btn {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background-color: color-mix(in srgb, var(--color-bg-surface) 85%, transparent);
    color: var(--color-text);
    border: 0;
    font-size: 18px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 4;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    transition: background-color var(--transition-fast),
        color var(--transition-fast),
        transform var(--transition-fast);
}

.remove-active-media-btn:hover {
    background-color: var(--color-danger, #dc3545);
    color: white;
    transform: scale(1.05);
}

.remove-active-media-btn:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent) 40%, transparent);
}

@media (prefers-reduced-motion: reduce) {
    .remove-active-media-btn {
        transition: none;
    }
}

/* ============================================================
   Media thumb polish — enter/leave animations + zoom modal (pr12)
   ============================================================
   Builds on the existing .media-thumb-edit / .remove-media-btn rules.
   Adds: responsive sizing on small screens, fade-in on add, fade-out
   on remove, larger touch hit-target for the remove button, and the
   chrome-less zoom modal that opens on thumb click.
   ============================================================ */

/* === Responsive sizing ====================================
   Override the fixed 80×80 in the existing .media-thumb-edit rule
   on narrow viewports. clamp() gives us a smooth shrink from
   96px on tablets to 64px on small phones. */
.media-thumb-edit {
    width: clamp(64px, 18vw, 96px);
    height: clamp(64px, 18vw, 96px);
    /* Both the enter and leave transitions ride on opacity + transform.
       Defined here so the base state has a transition to animate against. */
    transition: opacity 240ms ease-out, transform 240ms ease-out;
    opacity: 1;
    transform: scale(1);
}

/* === Enter animation =====================================
   .media-thumb-enter is added on creation, then removed on the
   next animation frame by JS. CSS transitions back to the base
   state, which is opacity:1 + scale(1). */
.media-thumb-edit.media-thumb-enter {
    opacity: 0;
    transform: scale(0.85);
}

/* === Leave animation =====================================
   .media-thumb-leaving is added by fadeOutAndRemove() before
   the DOM element gets removed. */
.media-thumb-edit.media-thumb-leaving {
    opacity: 0;
    transform: scale(0.85);
    pointer-events: none;
}

/* === Touch hit-target ====================================
   On touch devices, the 20×20 remove button is genuinely hard to
   tap accurately. Bump to 28×28 — Apple's HIG recommends 44pt
   minimum, but 28px is the largest we can go without the button
   overlapping the thumb on small viewports. The pseudo-element
   technique would let us extend the hit area beyond the visual
   size, but for now this is enough.

   Desktop keeps the original compact 20×20 (defined in the existing
   .remove-media-btn rule). */
@media (hover: none) {
    .remove-media-btn {
        width: 28px;
        height: 28px;
        font-size: var(--font-size-lg);
        top: -8px;
        right: -8px;
    }
}

/* === Zoomable image affordance ===========================
   Visual hint that the image is clickable. Subtle scale-up on
   hover (mouse) and focus (keyboard), pointer cursor everywhere. */
.media-thumb-edit img[data-zoomable="true"] {
    cursor: zoom-in;
    transition: transform var(--transition-fast),
        box-shadow var(--transition-fast);
}

.media-thumb-edit img[data-zoomable="true"]:hover {
    transform: scale(1.05);
}

/* Keyboard focus ring — same pattern as nav/carousel. */
.media-thumb-edit img[data-zoomable="true"]:focus-visible {
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent) 40%, transparent);
    transform: scale(1.05);
}

/* === Zoom modal ===========================================
   Override Bootstrap's default modal chrome. We want a dim
   background + the image floating in the center, no card body,
   no header, no footer. */

.media-zoom-modal .modal-dialog {
    /* modal-lg is 800px max-width; this lets us go wider on big
       screens without overflowing. */
    max-width: min(90vw, 1200px);
    margin: var(--space-4) auto;
}

.media-zoom-modal .modal-content.media-zoom-content {
    background-color: transparent;
    border: 0;
    box-shadow: none;
    align-items: center;
    /* The image inside is centered; the modal-content collapses
       to fit the image's intrinsic size. */
}

/* When THIS modal is open, darken the backdrop. The `body.media-zoom-open`
   class is added/removed by our JS so the rule only applies during
   actual zoom-modal display, not during dismissal animation. This
   avoids fighting Bootstrap's cleanup logic. */
body.media-zoom-open .modal-backdrop {
    opacity: 0.85;
}

/* The close button — sits absolute top-right of the image,
   white so it's visible against any image. Overrides Bootstrap's
   default .btn-close (which is a dark X intended for white-card
   modals). */
.media-zoom-close {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    z-index: 2;
    /* The default btn-close is a small SVG-background X; we want
       it light-on-dark for visibility against arbitrary images. */
    filter: invert(1) brightness(1.2);
    /* Subtle dark backing for contrast against light images. */
    background-color: rgba(0, 0, 0, 0.4);
    border-radius: 50%;
    padding: var(--space-2);
    opacity: 0.9;
    transition: opacity var(--transition-fast), transform var(--transition-fast);
}

.media-zoom-close:hover,
.media-zoom-close:focus-visible {
    opacity: 1;
    transform: scale(1.1);
    outline: none;
}

/* === The zoomed image ====================================
   Constrain to the viewport so massive full-res phone photos
   don't overflow. object-fit keeps aspect ratio intact. */
.media-zoom-image {
    max-width: 100%;
    max-height: 85vh;
    width: auto;
    height: auto;
    object-fit: contain;
    border-radius: var(--radius-md);
    /* Subtle shadow lifts the image off the dark backdrop. */
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}

/* === Reduced motion ======================================
   Disable all the polish transitions for users with the OS
   preference set. The DOM removal still happens (via the JS
   setTimeout fallback), the fade just becomes instant. */
@media (prefers-reduced-motion: reduce) {

    .media-thumb-edit,
    .media-thumb-edit img[data-zoomable="true"],
    .media-zoom-close {
        transition: none;
    }

    .media-thumb-edit.media-thumb-enter,
    .media-thumb-edit.media-thumb-leaving {
        /* Skip the scale, keep the opacity change so JS still
           sees the element transition through opacity:0. */
        transform: none;
    }
}

/* ============================================================
   Avatar hover-preview popover (pr22)
   ============================================================ */

.avatar-popover {
    max-width: 280px;
}

.avatar-card {
    /* Bootstrap's .popover-body already provides padding. */
}

.avatar-card-bio {
    line-height: 1.4;
    color: var(--bs-body-color);
    word-break: break-word;
}

.avatar-card-stats {
    display: flex;
    align-items: baseline;
}

/* Avatar links — let the underlying avatar styling carry through;
   we just need to remove the default <a> underline / color tint and
   make the link inline with the avatar's wrapper sizing. */

.avatar-link {
    display: inline-block;
    text-decoration: none;
    color: inherit;
    line-height: 0;
    /* Prevents extra vertical space from inline-block + img baseline */
}

.avatar-link:hover,
.avatar-link:focus {
    text-decoration: none;
}

.avatar-card-loading {
    text-align: center;
    padding: var(--space-2);
}


/* ============================================================
   Folded in from style.css during css-refactor commit 8.
   Reaction cluster + reaction picker chrome + media zoom modal
   + drafts page + per-card draft badge — all feed concerns
   that were buried in unrelated sections of the original file.
   ============================================================ */

/* =========================================================
   Reaction cluster (pr25f)
   Trigger button + popover picker on post cards, post
   detail (full + panel), and comments. State-driven
   styling via .is-reacted + .reaction-<type> classes.
   ========================================================= */

.reaction-cluster {
    position: relative;
    display: inline-flex;
    align-items: center;
}

.reaction-trigger {
    background: transparent;
    border: none;
    padding: 4px 6px;
    border-radius: 999px;
    cursor: pointer;
    color: var(--bs-secondary-color, #6c757d);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    transition: background 0.15s ease, color 0.15s ease, transform 0.1s ease;
}

.reaction-trigger:hover {
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
}

.reaction-trigger:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent, #6c5ce7) 40%, transparent);
}

.reaction-trigger:active {
    transform: scale(0.92);
}

/* When the user has a reaction, the trigger shows the chosen emoji
   in a subtly tinted pill so it reads as "selected" rather than
   neutral. The per-emoji classes exist to support future per-emoji
   tinting; for v1 they share the same pill. */
.reaction-trigger.is-reacted {
    background: var(--bs-tertiary-bg);
}

.reaction-trigger.is-reacted:hover {
    background: var(--bs-secondary-bg);
}

.reaction-emoji {
    font-size: 16px;
    line-height: 1;
    /* Emoji glyphs render slightly low in their box on most platforms;
       a hair of translateY pulls them to optical center. */
    display: inline-block;
    transform: translateY(-1px);
}

.reaction-trigger-icon {
    /* The outline-heart SVG. Inherits stroke color from currentColor. */
    width: 18px;
    height: 18px;
}

/* === Picker popover === */

.reaction-picker {
    position: absolute;
    bottom: calc(100% + 6px);
    left: 0;
    z-index: 1050;
    display: flex;
    align-items: center;
    gap: 2px;
    padding: 4px 6px;
    background: color-mix(in srgb, var(--bs-body-bg) 92%, transparent);
    backdrop-filter: blur(10px) saturate(1.4);
    -webkit-backdrop-filter: blur(10px) saturate(1.4);
    border: 1px solid var(--bs-border-color);
    border-radius: 999px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
    /* Animate in when .is-open is added. Default state (hidden attr
       still controls actual visibility; this is just the entrance). */
    transform: translateY(4px) scale(0.95);
    opacity: 0;
    transition: transform 0.12s ease, opacity 0.12s ease;
    transform-origin: bottom left;
}

.reaction-picker.is-open {
    transform: translateY(0) scale(1);
    opacity: 1;
}

.reaction-option {
    background: transparent;
    border: none;
    padding: 6px 8px;
    border-radius: 999px;
    cursor: pointer;
    line-height: 1;
    transition: background 0.1s ease, transform 0.1s ease;
}

.reaction-option:hover {
    background: var(--bs-tertiary-bg);
    transform: scale(1.15);
}

.reaction-option:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent, #6c5ce7) 40%, transparent);
}

.reaction-option:active {
    transform: scale(0.95);
}

.reaction-option .reaction-emoji {
    font-size: 20px;
}

/* === Mobile tweaks ===
   On narrow viewports the picker can crowd a small post card. Drop
   the gap, slightly larger tap targets per Apple HIG (~44px). */
@media (max-width: 540px) {
    .reaction-option {
        padding: 8px 10px;
    }

    .reaction-option .reaction-emoji {
        font-size: 22px;
    }
}

/* === prefers-reduced-motion ===
   Skip the scale-and-fade entrance entirely. */
@media (prefers-reduced-motion: reduce) {
    .reaction-trigger,
    .reaction-option,
    .reaction-picker {
        transition: none;
    }

    .reaction-trigger:active,
    .reaction-option:hover,
    .reaction-option:active {
        transform: none;
    }
}

/* =========================================================
   Reaction roster modal (pr25f-followup-a)
   Owner-only "see who reacted" affordance + the modal
   body that lists reactors grouped by emoji.
   ========================================================= */

.reaction-roster-trigger {
    background: transparent;
    border: none;
    padding: 4px 6px;
    border-radius: 999px;
    cursor: pointer;
    color: var(--bs-secondary-color, #6c757d);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    margin-left: 2px;
    transition: background 0.15s ease, color 0.15s ease;
}

.reaction-roster-trigger:hover {
    background: var(--bs-tertiary-bg);
    color: var(--bs-body-color);
}

.reaction-roster-trigger:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-accent, #6c5ce7) 40%, transparent);
}

.reaction-roster-icon {
    width: 18px;
    height: 18px;
}

/* Notifications-page "see all reactions" link — sits inline next to
   the existing "view" link on reaction-kind notification rows. */
.notification-roster-link {
    margin-left: 0.5rem;
    color: var(--bs-secondary-color, #6c757d);
    text-decoration: none;
    font-size: 0.875rem;
}

.notification-roster-link:hover {
    color: var(--bs-link-hover-color);
    text-decoration: underline;
}

/* === Modal body === */

.reaction-roster-modal-body {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.reaction-roster-section {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.reaction-roster-section-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding-bottom: 0.25rem;
    border-bottom: 1px solid var(--bs-border-color);
}

.reaction-roster-section-header .reaction-roster-emoji {
    font-size: 24px;
    line-height: 1;
    /* Same optical-center nudge as reaction-emoji elsewhere. */
    display: inline-block;
    transform: translateY(-1px);
}

.reaction-roster-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.reaction-roster-row {
    display: flex;
    align-items: center;
    gap: 0.625rem;
    padding: 0.25rem 0;
}

.reaction-roster-name {
    display: flex;
    flex-direction: column;
    line-height: 1.3;
    color: var(--bs-body-color);
}

.reaction-roster-name:hover .reaction-roster-display-name {
    text-decoration: underline;
}

.reaction-roster-display-name {
    font-weight: 600;
}

.reaction-roster-handle {
    /* Inherits .text-muted .small from the template. */
}

.reaction-roster-empty {
    /* Inherits .text-muted .small from the partial; nothing extra needed. */
}

@media (prefers-reduced-motion: reduce) {
    .reaction-roster-trigger {
        transition: none;
    }
}

/* ===================================================================
 * === Media zoom modal (pr26)                                     ===
 * === SwiperJS carousel inside a Bootstrap modal. Handles 1 or 1+ ===
 * === images with arrow/swipe/keyboard navigation.                ===
 * =================================================================== */

body.media-zoom-open {
    overflow: hidden;
}

.media-zoom-modal .modal-content.media-zoom-content {
    background: transparent;
    border: 0;
    box-shadow: none;
}

.media-zoom-modal.show + .modal-backdrop,
.modal-backdrop.show:has(+ .media-zoom-modal),
.modal-backdrop.media-zoom-backdrop {
    opacity: 0.85;
}

.media-zoom-modal .media-zoom-close {
    position: absolute;
    /* mobile-audit followup: clear the Dynamic Island / status bar
       on iPhone PWA + Safari. Without safe-area-top the X sits at
       ~36px from viewport top and gets covered by the system chrome. */
    top: calc(0.75rem + var(--safe-area-top));
    right: calc(0.75rem + var(--safe-area-right));
    z-index: 10;
    background-color: rgba(255, 255, 255, 0.85);
    border-radius: 50%;
    padding: 0.5rem;
    opacity: 0.9;
}

.media-zoom-modal .media-zoom-close:hover,
.media-zoom-modal .media-zoom-close:focus-visible {
    opacity: 1;
}

/* Swiper container — fills the modal dialog, centers its slides. */
.media-zoom-modal .media-zoom-swiper {
    width: 100%;
    height: 100%;
    min-height: 60vh;
}

/* Each slide centers its image. */
.media-zoom-modal .swiper-slide {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1rem;
}

/* Image sizing — fit within the slide without cropping, preserve aspect. */
.media-zoom-modal .media-zoom-image {
    max-width: 100%;
    max-height: 85vh;
    object-fit: contain;
    border-radius: 0.375rem;
}

/* Nav arrows - circular, soft, sit just inside the slide edges. Hidden by inline style from JS when slideCount <= 1. */
.media-zoom-modal .media-zoom-prev,
.media-zoom-modal .media-zoom-next {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    z-index: 10;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: 50%;
    border: 0;
    background-color: rgba(255, 255, 255, 0.85);
    color: #000;
    font-size: 1.5rem;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    opacity: 0.85;
    transition: opacity 0.15s ease, background-color 0.15s ease;
}

.media-zoom-modal .media-zoom-prev:hover,
.media-zoom-modal .media-zoom-next:hover,
.media-zoom-modal .media-zoom-prev:focus-visible,
.media-zoom-modal .media-zoom-next:focus-visible {
    opacity: 1;
    background-color: rgba(255, 255, 255, 1);
}

.media-zoom-modal .media-zoom-prev {
    left: 1rem;
}

.media-zoom-modal .media-zoom-next {
    right: 1rem;
}

.media-zoom-modal .media-zoom-prev.swiper-button-disabled,
.media-zoom-modal .media-zoom-next.swiper-button-disabled {
    opacity: 0.35;
    cursor: default;
}

.media-zoom-modal .media-zoom-pagination {
    position: absolute;
    bottom: 1rem;
    left: 0;
    right: 0;
    text-align: center;
    z-index: 10;
}

.media-zoom-modal .swiper-pagination-bullet {
    background-color: rgba(255, 255, 255, 0.6);
    opacity: 1;
    width: 8px;
    height: 8px;
    margin: 0 4px;
}

.media-zoom-modal .swiper-pagination-bullet-active {
    background-color: #fff;
}

[data-zoomable="true"] {
    cursor: zoom-in;
}

[data-zoomable="true"]:focus-visible {
    outline: 2px solid var(--color-accent, #6f4ce8);
    outline-offset: 2px;
}

@media (max-width: 575.98px) {
    .media-zoom-modal .modal-dialog {
        margin: 0;
        height: 100vh;
        height: 100dvh;
    }

    .media-zoom-modal .media-zoom-swiper {
        min-height: 100vh;
        min-height: 100dvh;
    }

    .media-zoom-modal .media-zoom-image {
        max-height: 80vh;
        max-height: 80dvh;
    }
}

@media (prefers-reduced-motion: reduce) {
    .media-zoom-modal .media-zoom-prev,
    .media-zoom-modal .media-zoom-next,
    .media-zoom-modal .media-zoom-close {
        transition: none;
    }
}


/* === Drafts page (se14-followup-a) ==============================
   Visual treatment for the standalone /drafts page. Sibling of
   .inbox-page (pr39) and .notifications-page (pr31) — same chrome
   anatomy (max-width-capped wrapper, h1 title + accent count pill,
   shared empty-state idiom).

   Cards inside .drafts-list inherit pr33's hybrid card chrome via
   selector extensions in the pr33 section above (`.drafts-list .card`
   added comma-sibling to the existing `.profile-feed .card` rules).
   The hover halo on .drafts-list .card is unconditional (no
   .panel-clickable gate) because drafts cards aren't panel-openable
   — the timestamp permalink navigates to /post/<id> as a full page,
   and the halo communicates "interactive" at card scale.

   Per-page signature: the .post-draft-badge that appears in every
   draft card's author row. Tracked-out uppercase pill in muted
   color, same idiom family as .links-column-tile-domain and
   .account-rail-label. Defined here rather than next to .post-body
   because it's drafts-feature copy, not generic card chrome.

   Companion rules elsewhere in this file that interact with these:
     - `.inbox-page` (~line ~) — the visual reference; .drafts-page
       chrome is byte-for-byte equivalent in shape, with the action
       button slot omitted (no `+ new draft` CTA on this page).
     - pr33 selectors above — extended in place to cover .drafts-list .card.
   ================================================================ */

.drafts-page {
    max-width: 720px;
    margin: 0 auto;
    padding: var(--space-5) var(--space-3);
}

.drafts-page-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-4);
}

.drafts-page-title {
    font-size: 1.5rem;
    font-weight: var(--font-weight-semibold);
    margin: 0;
    letter-spacing: -0.02em;
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.drafts-count-pill {
    display: inline-flex;
    align-items: baseline;
    padding: 2px 10px;
    background: color-mix(in srgb, var(--color-accent) 14%, transparent);
    color: var(--color-accent);
    border-radius: var(--radius-pill);
    font-size: 0.75rem;
    font-weight: var(--font-weight-semibold);
    font-variant-numeric: tabular-nums;
}

/* The list itself is just a flex column. Its primary job is to be
   the scope hook for pr33's extended selectors (see banner above). */
.drafts-list {
    display: flex;
    flex-direction: column;
}

/* Empty state — same idiom as .messages-empty / .notifications-empty.
   Quiet centered prompt, two-line title + sub-CTA structure. */
.drafts-empty {
    text-align: center;
    padding: var(--space-6) var(--space-4);
    color: var(--color-text-muted);
}

.drafts-empty-title {
    margin: 0 0 var(--space-1) 0;
    font-weight: var(--font-weight-semibold);
    font-size: 0.95rem;
    color: var(--color-text);
}

.drafts-empty-cta {
    margin: 0;
    font-size: var(--font-size-base);
    line-height: 1.5;
}

/* === Per-card draft badge ===
   Small tracked-out uppercase pill rendered next to the post
   timestamp when post.is_draft is True (and SE14_DRAFTS_ENABLED
   is on). Visible anywhere a draft renders — today that's only
   the drafts page and the post-detail view of a user's own draft,
   since visible_posts_query blocks drafts from non-author viewers.
   Tracked-out idiom family: .links-column-tile-domain,
   .account-rail-label, .admin-rail-label. */
.post-draft-badge {
    display: inline-flex;
    align-items: center;
    margin-left: var(--space-2);
    padding: 1px 7px;
    font-size: var(--font-size-xs);
    font-weight: var(--font-weight-semibold);
    color: var(--color-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.08em;
    border: 1px solid var(--color-border);
    border-radius: var(--radius-pill);
    line-height: 1.5;
    vertical-align: baseline;
}

/* Responsive — same shape as the inbox page on narrow screens.
   Title stack alone (no action button to wrap around), but tighten
   outer padding so the page breathes correctly on phones. */
@media (max-width: 600px) {
    .drafts-page {
        padding: var(--space-3) var(--space-2);
    }

    .drafts-page-header {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
    }
}

/* Whole-card click target on the drafts page.
   The card itself isn't a link (nested <a>/<button> children make
   that invalid HTML — same constraint dashboard cards face), so the
   click is wired via delegated JS in drafts.html. The pointer cursor
   here is the visual affordance.

   Mirrors the .dashboard-grid .panel-clickable rule (~line 2110).
   Nested interactives (timestamp permalink, action-row links,
   three-dot menu, reactions) keep their default cursor via the
   browser's natural inheritance from <a>/<button> defaults. */
.drafts-list .card {
    cursor: pointer;
}

