/*
 * media-grid.css — a grid of video/thumbnail cards inside a .ds-modal.
 *
 * First consumer: the All Workout Videos popup (#id04). Consumes tokens.css and
 * modal.css; load after both. Every selector is .ds-media-* or scoped under
 * .ds-modal-media, so nothing outside the converted popup changes.
 *
 * The cards are written by all.js renderPage() as template strings. Their
 * vendor classes (.workout-videos2 .vids-sec .wpallvideo) stay on the same
 * elements, so dashboard.css's D4 ellipsis rule keeps matching and the legacy
 * style.css rules for those classes still apply underneath — the rules here
 * override the ones that matter (the 200px fixed height and 5px grey border on
 * .wpallvideo) by specificity. The vendor's play badge (.wpplaybtn) is no
 * longer emitted — Mark and Jenny asked for plain thumbnails (2026-09-27); its
 * style.css:4573 rule is left as dead vendor CSS.
 *
 * Added 2026-09-22 (Workouts popup, D3).
 */


/* ===========================================================================
 * Toolbar — the search field, pinned between the head and the scrolling body.
 * ======================================================================== */

.ds-media-toolbar {
    flex: 0 0 auto;
    padding: 0 var(--ds-space-5) var(--ds-space-3);
}

/*
 * The pill itself is `.ds-search` in components.css (promoted from here on
 * 2026-09-24 so the Recipes page and this popup share one field). The only
 * thing this popup adds is its accent on focus: components.css rings the pill
 * in --ds-teal; inside a popup the ring follows the popup's own accent.
 */
.ds-modal-media .ds-search:focus-within {
    border-color: var(--ds-modal-accent, var(--ds-teal));
    outline-color: var(--ds-modal-accent, var(--ds-teal));
}

/* The body's top padding is meant to clear a head; here the toolbar does that. */
.ds-modal-media .ds-modal-body {
    padding-top: var(--ds-space-1);
}


/* ===========================================================================
 * Grid
 *
 * 2 across on a phone, 3 from 768, 4 from 1024. Fixed counts, not auto-fit:
 * a card that changes size with how many there are is the defect Mark reported
 * on the service tiles (#395). `minmax(0, 1fr)` so a long title can never widen
 * a track (the F8 lesson).
 *
 * `.wp_videos` is a <span> with Bootstrap's `.row` on it; `display: grid`
 * overrides the flex row, and the negative margins `.row` carries are zeroed.
 * ======================================================================== */

.ds-modal-media .ds-media-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--ds-space-3);
    width: 100%;
    margin: 0;
}

@media (min-width: 768px) {
    .ds-modal-media .ds-media-grid {
        grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: var(--ds-space-4);
    }
}

@media (min-width: 1024px) {
    .ds-modal-media .ds-media-grid {
        grid-template-columns: repeat(4, minmax(0, 1fr));
    }
}

/* Vendor left a second, always-empty .row span in the body; keep it out of flow. */
.ds-modal-media #search_workout_video:empty {
    display: none;
}

/*
 * The dialog is phone-width under 768 and 560px above (modal.css). Four 16:9
 * cards need more than 560 to read; give this popup a wider desktop dialog.
 * Still capped so it is a dialog, not a page.
 */
@media (min-width: 1024px) {
    .ds-modal-media .ds-modal-dialog {
        /* `width`, not `max-width`: modal.css sizes the dialog with
           `width: min(560px, 100%)` from 768, so a max-width alone changes nothing. */
        width: min(880px, 100%);
    }
}


/* ===========================================================================
 * Card
 *
 *   <div class="ds-media-card">
 *     <a class="workout-videos2" ...>
 *       <span class="ds-media-thumb"><img class="wpallvideo"></span>
 *       <div class="vids-sec">Title</div>
 *     </a>
 *   </div>
 * ======================================================================== */

.ds-media-card {
    min-width: 0;
}

/* `.workout-videos2`, not `a.workout-videos2`: the loading skeleton uses the same
   card markup with a <span> in place of the link, and must get the same box. */
.ds-modal-media .ds-media-card > .workout-videos2 {
    position: relative;
    display: block;
    overflow: hidden;
    border-radius: var(--ds-radius-md);
    background: var(--ds-surface);
    box-shadow: var(--ds-shadow-sm);
    color: var(--ds-text);
    text-decoration: none;
    -webkit-tap-highlight-color: transparent;
}

.ds-modal-media .ds-media-card > .workout-videos2:hover,
.ds-modal-media .ds-media-card > .workout-videos2:focus-visible {
    box-shadow: var(--ds-shadow-md);
    outline: 0;
}

.ds-modal-media .ds-media-card > .workout-videos2:focus-visible {
    outline: 2px solid var(--ds-modal-accent, var(--ds-teal));
    outline-offset: 2px;
}

/*
 * 4:3 thumbnail, and the image never dictates the card's height, so a missing
 * or odd-sized thumbnail cannot break the row.
 *
 * 4:3, not the 16:9 the mockup's photo cards suggest: every one of the 22
 * production stills is 1448x1086 (measured 2026-09-23), and they are
 * typographic cards — programme name and tagline centred on cream — so a 16:9
 * centre crop would take 12.5% off top and bottom and run within a few percent
 * of the tagline. Matching the artwork's ratio crops nothing. `cover` stays so
 * an odd-sized upload (staging has several) still fills the box.
 *
 * Padding-ratio box with the image absolutely positioned, NOT `aspect-ratio`
 * plus `height: 100%` on the image. A percentage height resolves against a
 * parent height that is *definite*, and Safari and older WebViews (the app
 * ships inside one) treat an aspect-ratio-derived height as indefinite: the
 * image then falls back to its own size and sits at natural aspect inside the
 * box instead of covering it. Percentage sizes on an absolutely positioned
 * child resolve against the containing block in every engine.
 */
.ds-media-thumb {
    position: relative;
    display: block;
    height: 0;
    padding-top: 75%;
    overflow: hidden;
    /* Neutral, not the accent tint: this shows behind a loading or missing
       thumbnail, and a block of colour there read as part of the design. */
    background: var(--ds-divider);
}

.ds-modal-media .ds-media-thumb img.wpallvideo {
    position: absolute;
    inset: 0;
    display: block;
    width: 100%;
    height: 100%;
    min-height: 0 !important; /* style.css:8683 sets 355px !important under 600px */
    border: 0;
    object-fit: cover;
}

/*
 * Broken image — S3 URLs 403 after six days, so this is a routine state, not an
 * edge. The <img> hides itself (onerror adds .is-broken to the wrapper), leaving
 * the neutral tile under the play badge; the title beneath still reads. No
 * second glyph — the badge already says "video".
 */
.ds-media-thumb.is-broken img.wpallvideo {
    display: none;
}

/* Caption band under the thumbnail. dashboard.css's D4 rule supplies the
   one-line ellipsis; this supplies the rest. The inline colour/size/weight the
   old template strings carried are gone, so these apply. */
.ds-modal-media .ds-media-card .vids-sec {
    display: block;
    padding: var(--ds-space-2) var(--ds-space-3);
    font-size: var(--ds-size-sm);
    font-weight: var(--ds-weight-semibold);
    color: var(--ds-text);
    text-align: left;
}

.ds-media-empty {
    grid-column: 1 / -1;
    margin: var(--ds-space-6) 0;
    text-align: center;
    color: var(--ds-text-muted);
}


/* ===========================================================================
 * Pager — the vendor's markup, restyled as pills. Sits below the body like a
 * footer, and only renders when there is more than one page (all.js).
 * ======================================================================== */

.ds-modal-media .ds-media-pager {
    flex: 0 0 auto;
    padding: 0;
    border-top: 0;
}

.ds-modal-media .ds-media-pager:empty {
    display: none;
}

.ds-modal-media .ds-media-pager .pagination {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--ds-space-2);
    margin: 0;
    padding: var(--ds-space-3) var(--ds-space-5);
    padding-bottom: calc(var(--ds-space-3) + env(safe-area-inset-bottom, 0px));
    border-top: 1px solid var(--ds-divider);
    background: var(--ds-surface);
}

/* 44px pills — foundation.css floors bare buttons to 44px under 768 anyway;
   stating it here keeps them the same size above 768 too. */
.ds-modal-media .ds-media-pager .pagination button.pagination-btn {
    min-width: 44px;
    height: 44px;
    width: auto;
    padding: 0 var(--ds-space-3);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-pill);
    background: var(--ds-surface);
    color: var(--ds-text);
    font-family: var(--ds-font);
    font-size: var(--ds-size-sm);
    font-weight: var(--ds-weight-semibold);
    cursor: pointer;
}

.ds-modal-media .ds-media-pager .pagination button.pagination-btn.active {
    border-color: var(--ds-modal-accent, var(--ds-teal));
    background: var(--ds-modal-accent, var(--ds-teal));
    color: #fff;
}

.ds-modal-media .ds-media-pager .pagination button.pagination-btn.disabled {
    background: var(--ds-surface);
    color: var(--ds-text-muted);
    opacity: 0.5;
    cursor: default;
}


/* ===========================================================================
 * The player — a second .ds-modal, opened from INSIDE the first.
 *
 * Above #id04's 10000 and below SweetAlert's 10020 (modal.css). Was a
 * Bootstrap modal lifted to 10015 in #408; converting it to the component is
 * what makes the two dialogs the client sees back to back look like one design.
 * js/modal.js closes the LAST open .ds-modal on Escape / backdrop, and the
 * player's markup follows #id04, so the player closes first — no JS change.
 * ======================================================================== */

.ds-modal-player {
    z-index: 10015;
}

/* Wider than the 560px default from 768: a 16:9 video at 560 is small. Same
   cap as the grid popup, so the pair read as one system. */
@media (min-width: 1024px) {
    .ds-modal-player .ds-modal-dialog {
        width: min(880px, 100%);
    }
}

/*
 * The frame. Two kinds of embed, told apart by the class playVideo() sets:
 *
 *   .youtube-video  — a real player: a 16:9 padding-ratio box, the iframe
 *                     absolutely filling it (engine-safe, as the card thumbs).
 *   .vimeo-video    — on this site these are Vimeo SHOWCASE embeds, a tall
 *                     playlist widget ("62 videos · Autoplay next"), not a
 *                     player. A 16:9 box would clip its list, so it gets a
 *                     tall frame inside the component's scrolling body: the head
 *                     stays pinned and the widget scrolls within the sheet
 *                     instead of running off the page (the 09-24 phone report).
 *
 * The old fixed heights and 60/120px top margins in style.css:9534-9560 were
 * keyed on .modal-dialog and no longer match.
 */
.ds-player-frame {
    position: relative;
    width: 100%;
    background: #000;
    border-radius: var(--ds-radius-md);
    overflow: hidden;
}

/*
 * Vimeo SHOWCASE embeds are a whole web page inside the iframe. On a 353px
 * frame its layout overflowed its own viewport once playback started, iOS let
 * a finger pan it, and the black showed in the exposed area (Marcus's iPhone,
 * 2026-09-24). Two things we control: give it the full sheet width under 768
 * (390 instead of 353), and put a neutral surface behind it — black is right
 * for a letterboxed YouTube player, wrong behind a light Vimeo page.
 */
.ds-player-frame:has(> #videoFrame.vimeo-video) {
    background: var(--ds-surface);
}

@media (max-width: 767.98px) {
    .ds-modal-player .ds-modal-body {
        padding-inline: 0;
    }

    .ds-modal-player .ds-player-frame {
        border-radius: 0;
    }

    /*
     * The player opens OVER the grid popup, and a content-sized player sheet
     * was shorter than the grid's — the "All Workout Videos" head peeked out
     * above it (Marcus, 09-24). The player sheet takes the grid's cap, 92dvh,
     * so it is always at least as tall; the grid is at that cap with 16 cards
     * and shorter with fewer, in which case the player is simply taller.
     * `vh` first for engines without `dvh`, as in modal.css.
     */
    .ds-modal-player .ds-modal-dialog {
        height: 92vh;
        height: 92dvh;
    }

    /* With a fixed sheet height the frame fills the body rather than leaving a
       strip of surface under it. The Vimeo showcase is a page, so it takes all
       of it; the YouTube 16:9 box keeps its ratio and centres. */
    .ds-modal-player .ds-modal-body {
        display: flex;
        flex-direction: column;
    }

    .ds-player-frame:has(> #videoFrame.vimeo-video) {
        display: flex;
        flex-direction: column;
        flex: 1 1 auto;
        min-height: 0;
    }

    .ds-modal-player #videoFrame.vimeo-video {
        flex: 1 1 auto;
        height: auto;
        min-height: 0;
    }

    .ds-player-frame:has(> #videoFrame.youtube-video) {
        margin-block: auto;
    }
}

/* From 768 the two are centred cards and a taller grid card shows around the
   player's edges; hiding the grid's card while the player is open is the honest
   version of "the client cannot tell the two apart". The backdrop (the popup
   itself) stays, so the lock count and the dimming are untouched; `[style*=flex]`
   is the inline `display: flex` the component is opened with (modal.css). */
@media (min-width: 768px) {
    .ds-modal-media:has(+ .ds-modal-player[style*="flex"]) .ds-modal-dialog {
        visibility: hidden;
    }
}

.ds-modal-player #videoFrame {
    display: block;
    width: 100%;
    border: 0;
}

.ds-player-frame:has(> #videoFrame.youtube-video) {
    height: 0;
    padding-top: 56.25%;
}

.ds-modal-player #videoFrame.youtube-video {
    position: absolute;
    inset: 0;
    height: 100%;
}

.ds-modal-player #videoFrame.vimeo-video {
    height: min(70dvh, 640px);
    min-height: 320px;
}

/*
 * Short viewports (landscape phones): the 16:9 box is width-driven, so at
 * 844x390 it was 294px tall inside a 265px body and the player's controls sat
 * under the fold (visual QA F3); the Vimeo frame's 320px minimum did the same.
 * Drop the ratio there and size both frames to the height that is left;
 * YouTube letterboxes inside it, the showcase scrolls inside its own iframe.
 */
@media (max-height: 520px) {
    .ds-player-frame:has(> #videoFrame.youtube-video) {
        height: auto;
        padding-top: 0;
    }

    .ds-modal-player #videoFrame.youtube-video,
    .ds-modal-player #videoFrame.vimeo-video {
        position: static;
        height: calc(100dvh - 150px);
        min-height: 180px;
    }
}

/* On a landscape-phone SHEET the Vimeo frame is flex-filled (above), and the
   180px floor here overshot the 179px it was given at 568x320 by a pixel that
   `overflow: hidden` then clipped. The fill is the floor there. */
@media (max-width: 767.98px) and (max-height: 520px) {
    .ds-modal-player #videoFrame.vimeo-video {
        min-height: 0;
    }
}

/* Fallback for engines without :has() — the YouTube frame still gets a sane
   height rather than the attribute default. */
@supports not selector(:has(a)) {
    .ds-modal-player #videoFrame.youtube-video {
        position: static;
        height: min(56.25vw, 495px);
    }
}


/* ===========================================================================
 * Loading and error states
 *
 * Every request on staging takes ~1s to first byte (ISSUE-015), and the popup
 * used to open as an empty grid until the JSON arrived. all.js now prefetches
 * the list on dashboard load and, when a tap still beats it, renders eight
 * skeleton cards so the popup opens SHAPED — same grid, same tile ratio, so
 * nothing jumps when the real cards replace them.
 * ======================================================================== */

.ds-media-card.is-skeleton .ds-media-thumb {
    background: var(--ds-divider);
}

.ds-media-card.is-skeleton .vids-sec {
    /* A caption-shaped bar instead of text. */
    height: calc(var(--ds-size-sm) * 1.5);
    width: 60%;
    margin: var(--ds-space-2) var(--ds-space-3);
    padding: 0;
    border-radius: var(--ds-radius-pill);
    background: var(--ds-divider);
}

.ds-media-card.is-skeleton .ds-media-thumb,
.ds-media-card.is-skeleton .vids-sec {
    animation: ds-media-shimmer 1.2s ease-in-out infinite;
}

@keyframes ds-media-shimmer {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}

@media (prefers-reduced-motion: reduce) {
    .ds-media-card.is-skeleton .ds-media-thumb,
    .ds-media-card.is-skeleton .vids-sec {
        animation: none;
    }
}

.ds-media-error {
    grid-column: 1 / -1;
    margin: var(--ds-space-6) 0;
    text-align: center;
    color: var(--ds-text-muted);
}

.ds-media-error a {
    color: var(--ds-modal-accent, var(--ds-teal));
    font-weight: var(--ds-weight-semibold);
    text-decoration: underline;
    /* 44px hit area on an inline link. */
    display: inline-block;
    padding: var(--ds-space-3);
    margin: calc(var(--ds-space-3) * -1) 0;
}
