mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 12:36:15 -08:00
* feat(dashboard): TMDB trending rail and hero enrichment (backdrop, badges, S/E)
Two dashboard additions, both async after first paint so the page renders
exactly as fast as before:
Trending rail ("Trending this week", dashboardRails.tmdbTrending toggle,
default on, rendered only when TMDB is opted in AND the Electron DB
worker is available):
- TmdbTrendingService fetches /trending/{movie,tv}/week (one request
each, cached one day per language in tmdb_metadata under
trending:week), merges by popularity; exposed via the enrichment
facade (getTrendingWeek)
- DashboardTrendingService matches the titles against imported Xtream
playlists with ONE batched DB_MATCH_TITLES request, applying the same
two-tier + year-compatibility rule as actor pages; matched cards show
the playlist name and navigate straight to the detail view, unmatched
cards open the global search prefilled (?q=)
- The load fires only after the dashboard's own recent/favorites data
is in (never competes for the worker at startup) and once per session
- DashboardRailCard gained optional queryParams for the search links
Hero enrichment:
- DashboardHeroTmdbService patches the hero with a TMDB backdrop (only
when the item has none), a rating badge and up to two genre chips —
via the enrichment facade, so previously opened items resolve from
the SQLite cache without network; memoized per title per session,
staleness-guarded against hero changes in flight
- Series heroes show the tracked "S{n}·E{n}" badge from the playback
position; the watch-progress bar no longer applies to live heroes
Settings: new dashboardRails.tmdbTrending toggle in Settings > Dashboard.
i18n: 3 new keys translated into all 17 locales via tools/i18n patches.
Tests: dashboard-trending.service.spec.ts (gating, matching, year guard,
single-flight); settings fixtures updated. Docs updated
(tmdb-metadata-enrichment.md Dashboard Integration section, CLAUDE.md).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): resolve hero TMDB extras for Stalker embedded-series items
Stalker vclub items carry type 'movie' in activity rows but are TV shows
on TMDB, so the hero's movie lookup found no confident match and the
backdrop/badges never appeared — while the detail view (which resolves
via is_series) showed them. When a movie-typed hero item has no movie
match, retry the lookup as TV: the detail view has usually already
cached that resolution, and misses are negative-cached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(tmdb): cross-portal "Similar" rail — Stalker gets it, Xtream gains other-portal matches
The Similar rail only existed on Xtream because it matched against the
locally loaded catalog; Stalker catalogs are server-paginated, so its
detail views had no rail despite tmdb_recommendations being cached.
New CrossPortalSimilarService (libs/services) matches recommendations
against ALL imported Xtream playlists with one batched DB_MATCH_TITLES
worker request — the same two-tier normalized-title + year-compatibility
rule as actor pages and the trending rail. Electron-only; resolves to []
in the PWA.
- Stalker: the shared VodDetailsComponent (movies; covers catalog and
inline detail hosts) and stalker-series-view (series) now render a
"Similar" rail from cross-portal matches, each card badged with the
source playlist and navigating into that portal's detail view.
- Xtream: vod/serial detail rails keep instant local-catalog matches and
append cross-portal matches (current playlist excluded, deduplicated
against local hits by normalized exact title), also playlist-badged.
- Loads async after the detail view renders, staleness-guarded; the
section only appears when there is something to show.
Tests: cross-portal-similar.service.spec.ts (PWA gate, navigation
targets, playlist exclusion, type/year guards). Docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(tmdb): drop TestBed from cross-portal similar spec
The services Jest target has no @angular/core/testing (same CI failure
as the cache spec earlier) — construct the service via Injector.create +
runInInjectionContext instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(dashboard): address PR review — reactive opt-out gates, retry after empty trending load
- Trending rail and hero TMDB extras now vanish immediately when the
TMDB opt-in is switched off mid-session: the render computeds read the
settings signal through isAvailable/isEnabled instead of trusting data
loaded earlier (Codex P2 ×2).
- loadedOnce latches only after a successful non-empty load, so a
transient TMDB outage on first visit no longer suppresses the rail for
the whole session — the next dashboard visit retries (greptile P2).
- Unified the duplicated heroTmdbExtras() read in the hero computed
(greptile P2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
709 lines
20 KiB
SCSS
709 lines
20 KiB
SCSS
// ============================================================================
|
|
// Netflix-style Detail View - Cinematic Streaming Design
|
|
// ============================================================================
|
|
|
|
@use 'detail-view-actions' as detail-actions;
|
|
|
|
@mixin base(
|
|
$season-title-font-size: 1.375rem,
|
|
$season-title-font-weight: 600,
|
|
$season-title-letter-spacing: 0,
|
|
$episode-title-font-size: 1.125rem,
|
|
$episode-title-letter-spacing: 0
|
|
) {
|
|
:host {
|
|
--poster-width: 220px;
|
|
--poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.15);
|
|
--content-max-width: 1400px;
|
|
|
|
// Light theme colors (default - no .dark-theme class on body)
|
|
--hero-gradient-start: rgba(255, 250, 245, 0);
|
|
--hero-gradient-mid: rgba(255, 250, 245, 0.75);
|
|
--hero-gradient-end: rgba(255, 250, 245, 1);
|
|
--surface-bg: #fffaf5;
|
|
--text-primary: #1a1a1a;
|
|
--text-secondary: rgba(26, 26, 26, 0.75);
|
|
--text-muted: rgba(26, 26, 26, 0.5);
|
|
// Single primary CTA color — matches --app-selection-color so the
|
|
// Play button reads as the same blue used everywhere else for
|
|
// "active / call to action".
|
|
--accent-color: #2f7bff;
|
|
--tag-bg: rgba(0, 0, 0, 0.06);
|
|
--tag-border: rgba(0, 0, 0, 0.12);
|
|
--play-idle-bg: linear-gradient(
|
|
135deg,
|
|
#2f7bff 0%,
|
|
#3b86ff 52%,
|
|
#4a90ff 100%
|
|
);
|
|
--play-idle-fg: #ffffff;
|
|
--play-idle-border: rgba(47, 123, 255, 0.4);
|
|
--play-idle-shadow: 0 18px 38px rgba(47, 123, 255, 0.32);
|
|
--play-launching-bg: linear-gradient(
|
|
135deg,
|
|
#ffd36f 0%,
|
|
#ffb14c 46%,
|
|
#f57d35 100%
|
|
);
|
|
--play-launching-fg: #241103;
|
|
--play-launching-border: rgba(142, 66, 0, 0.2);
|
|
--play-launching-shadow: 0 22px 44px rgba(244, 136, 55, 0.32);
|
|
--play-stop-bg: linear-gradient(
|
|
135deg,
|
|
#ffd1c8 0%,
|
|
#ff9f8d 42%,
|
|
#ef5e64 100%
|
|
);
|
|
--play-stop-fg: #321012;
|
|
--play-stop-border: rgba(162, 38, 45, 0.18);
|
|
--play-stop-shadow: 0 22px 46px rgba(214, 74, 86, 0.26);
|
|
--play-icon-shell-bg: rgba(255, 255, 255, 0.32);
|
|
--play-icon-shell-border: rgba(255, 255, 255, 0.3);
|
|
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
overflow: hidden;
|
|
background: var(--surface-bg);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
// Dark theme overrides - cinematic dark mode
|
|
:host-context(.dark-theme) {
|
|
--hero-gradient-start: rgba(20, 20, 20, 0);
|
|
--hero-gradient-mid: rgba(20, 20, 20, 0.7);
|
|
--hero-gradient-end: rgba(20, 20, 20, 1);
|
|
--surface-bg: #141414;
|
|
--text-primary: #fff;
|
|
--text-secondary: rgba(255, 255, 255, 0.7);
|
|
--text-muted: rgba(255, 255, 255, 0.5);
|
|
--accent-color: #78adff;
|
|
--tag-bg: rgba(255, 255, 255, 0.1);
|
|
--tag-border: rgba(255, 255, 255, 0.2);
|
|
--poster-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
|
|
--play-idle-bg: linear-gradient(
|
|
135deg,
|
|
#2f7bff 0%,
|
|
#4f8eff 58%,
|
|
#78adff 100%
|
|
);
|
|
--play-idle-fg: #ffffff;
|
|
--play-idle-border: rgba(120, 173, 255, 0.32);
|
|
--play-idle-shadow: 0 20px 42px rgba(47, 123, 255, 0.32);
|
|
--play-launching-bg: linear-gradient(
|
|
135deg,
|
|
#ffdd79 0%,
|
|
#ffb347 42%,
|
|
#ef6a3a 100%
|
|
);
|
|
--play-launching-fg: #1f1105;
|
|
--play-launching-border: rgba(255, 223, 136, 0.26);
|
|
--play-launching-shadow: 0 24px 50px rgba(255, 153, 71, 0.3);
|
|
--play-stop-bg: linear-gradient(
|
|
135deg,
|
|
#ffd7cf 0%,
|
|
#ff9b8b 40%,
|
|
#eb5961 100%
|
|
);
|
|
--play-stop-fg: #2d1012;
|
|
--play-stop-border: rgba(255, 205, 205, 0.24);
|
|
--play-stop-shadow: 0 24px 50px rgba(235, 89, 97, 0.24);
|
|
--play-icon-shell-bg: rgba(255, 255, 255, 0.28);
|
|
--play-icon-shell-border: rgba(255, 255, 255, 0.24);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Details Section - Content Elements
|
|
// ============================================================================
|
|
|
|
.details {
|
|
// Styles for projected content elements
|
|
|
|
&__tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 12px;
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
background: var(--tag-bg);
|
|
border: 1px solid var(--tag-border);
|
|
border-radius: 4px;
|
|
backdrop-filter: blur(8px);
|
|
|
|
&--rating {
|
|
color: #ffd700;
|
|
border-color: rgba(255, 215, 0, 0.3);
|
|
}
|
|
}
|
|
|
|
&__meta-item {
|
|
font-size: 0.875rem;
|
|
|
|
.label {
|
|
color: var(--text-muted);
|
|
font-weight: 400;
|
|
margin-bottom: 2px;
|
|
text-decoration: none;
|
|
display: block;
|
|
}
|
|
|
|
.value {
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
// Cast avatar chips (populated by TMDB enrichment)
|
|
&__cast-strip {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px 12px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
&__cast-chip {
|
|
align-items: center;
|
|
display: inline-flex;
|
|
gap: 6px;
|
|
max-width: 160px;
|
|
|
|
&--clickable {
|
|
cursor: pointer;
|
|
|
|
&:hover .details__cast-name {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
}
|
|
|
|
&__cast-avatar {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
height: 32px;
|
|
object-fit: cover;
|
|
width: 32px;
|
|
|
|
&--fallback {
|
|
align-items: center;
|
|
color: var(--text-muted);
|
|
display: inline-flex;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
justify-content: center;
|
|
text-transform: uppercase;
|
|
}
|
|
}
|
|
|
|
&__cast-name {
|
|
color: var(--text-primary);
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
// "Similar" rail (TMDB recommendations matched to the catalog)
|
|
&__similar {
|
|
margin-top: 24px;
|
|
|
|
h3 {
|
|
color: var(--text-primary);
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
margin: 0 0 12px;
|
|
}
|
|
}
|
|
|
|
&__similar-strip {
|
|
display: flex;
|
|
gap: 12px;
|
|
overflow-x: auto;
|
|
padding-bottom: 8px;
|
|
scrollbar-width: thin;
|
|
}
|
|
|
|
&__similar-card {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex-shrink: 0;
|
|
gap: 6px;
|
|
padding: 0;
|
|
text-align: left;
|
|
width: 110px;
|
|
|
|
&:hover .details__similar-poster {
|
|
transform: scale(1.04);
|
|
}
|
|
}
|
|
|
|
&__similar-poster {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border-radius: 8px;
|
|
height: 165px;
|
|
object-fit: cover;
|
|
transition: transform 0.15s ease;
|
|
width: 110px;
|
|
|
|
&--fallback {
|
|
align-items: center;
|
|
color: var(--text-muted);
|
|
display: inline-flex;
|
|
font-size: 2rem;
|
|
font-weight: 600;
|
|
justify-content: center;
|
|
text-transform: uppercase;
|
|
}
|
|
}
|
|
|
|
&__similar-title {
|
|
color: var(--text-primary);
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
// Source playlist badge on cross-portal similar cards
|
|
&__similar-source {
|
|
color: var(--text-muted);
|
|
font-size: 0.6875rem;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Action Buttons - Play, Favorite
|
|
// ============================================================================
|
|
|
|
.play-btn {
|
|
--play-btn-bg: var(--play-idle-bg);
|
|
--play-btn-fg: var(--play-idle-fg);
|
|
--play-btn-border: var(--play-idle-border);
|
|
--play-btn-shadow: var(--play-idle-shadow);
|
|
|
|
position: relative;
|
|
isolation: isolate;
|
|
overflow: hidden;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 24px 10px 18px;
|
|
min-height: 64px;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0;
|
|
background: var(--play-btn-bg);
|
|
background-size: 140% 140%;
|
|
background-position: 0% 50%;
|
|
color: var(--play-btn-fg);
|
|
border: 1px solid var(--play-btn-border);
|
|
border-radius: 16px;
|
|
box-shadow: var(--play-btn-shadow);
|
|
cursor: pointer;
|
|
transition:
|
|
transform 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
background-position 280ms ease,
|
|
box-shadow 260ms ease,
|
|
border-color 220ms ease,
|
|
color 220ms ease,
|
|
filter 220ms ease;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 1px;
|
|
border-radius: inherit;
|
|
background:
|
|
linear-gradient(
|
|
115deg,
|
|
rgba(255, 255, 255, 0.34),
|
|
transparent 36%
|
|
),
|
|
linear-gradient(
|
|
180deg,
|
|
rgba(255, 255, 255, 0.18),
|
|
transparent 62%
|
|
);
|
|
opacity: 0.7;
|
|
transition: opacity 220ms ease;
|
|
z-index: -1;
|
|
}
|
|
|
|
&:hover:not(:disabled) {
|
|
transform: translateY(-2px);
|
|
background-position: 100% 50%;
|
|
box-shadow:
|
|
var(--play-btn-shadow),
|
|
0 10px 20px rgba(255, 255, 255, 0.16) inset;
|
|
}
|
|
|
|
&:hover:not(:disabled)::before {
|
|
opacity: 0.95;
|
|
}
|
|
|
|
&:active:not(:disabled) {
|
|
transform: translateY(0);
|
|
box-shadow: var(--play-btn-shadow);
|
|
}
|
|
|
|
&:disabled {
|
|
cursor: progress;
|
|
opacity: 1;
|
|
filter: saturate(1.05);
|
|
}
|
|
|
|
mat-icon {
|
|
font-size: 24px;
|
|
width: 24px;
|
|
height: 24px;
|
|
transition:
|
|
transform 220ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
color 220ms ease,
|
|
filter 220ms ease;
|
|
}
|
|
|
|
&:hover:not(:disabled) .play-btn__icon {
|
|
transform: scale(1.08);
|
|
}
|
|
|
|
&__icon-shell {
|
|
flex: 0 0 auto;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 999px;
|
|
background: var(--play-icon-shell-bg);
|
|
border: 1px solid var(--play-icon-shell-border);
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.38),
|
|
0 6px 14px rgba(0, 0, 0, 0.08);
|
|
backdrop-filter: blur(10px);
|
|
transition:
|
|
transform 220ms ease,
|
|
background-color 220ms ease,
|
|
border-color 220ms ease,
|
|
box-shadow 220ms ease;
|
|
}
|
|
|
|
@include detail-actions.play-button-copy(var(--play-btn-fg));
|
|
|
|
&--state-launching,
|
|
&--state-stop {
|
|
color: var(--play-btn-fg);
|
|
}
|
|
|
|
&--state-launching {
|
|
--play-btn-bg: var(--play-launching-bg);
|
|
--play-btn-fg: var(--play-launching-fg);
|
|
--play-btn-border: var(--play-launching-border);
|
|
--play-btn-shadow: var(--play-launching-shadow);
|
|
animation: playButtonPulse 1.8s ease-in-out infinite;
|
|
|
|
.play-btn__icon-shell {
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.42),
|
|
0 10px 22px rgba(184, 89, 26, 0.2);
|
|
}
|
|
|
|
.play-btn__icon {
|
|
animation: playIconLaunch 1.15s cubic-bezier(0.55, 0, 0.45, 1)
|
|
infinite;
|
|
}
|
|
}
|
|
|
|
&--state-stop {
|
|
--play-btn-bg: var(--play-stop-bg);
|
|
--play-btn-fg: var(--play-stop-fg);
|
|
--play-btn-border: var(--play-stop-border);
|
|
--play-btn-shadow: var(--play-stop-shadow);
|
|
|
|
.play-btn__icon-shell {
|
|
box-shadow:
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.4),
|
|
0 12px 24px rgba(145, 38, 53, 0.18);
|
|
}
|
|
|
|
.play-btn__icon {
|
|
animation: playIconStop 1.2s ease-in-out infinite;
|
|
}
|
|
}
|
|
}
|
|
|
|
@keyframes playButtonPulse {
|
|
0%,
|
|
100% {
|
|
box-shadow: var(--play-btn-shadow);
|
|
}
|
|
|
|
50% {
|
|
box-shadow:
|
|
var(--play-btn-shadow),
|
|
0 0 0 6px rgba(255, 190, 92, 0.14);
|
|
}
|
|
}
|
|
|
|
@keyframes playIconLaunch {
|
|
0%,
|
|
100% {
|
|
transform: rotate(0deg) scale(1);
|
|
}
|
|
|
|
50% {
|
|
transform: rotate(18deg) scale(1.1);
|
|
}
|
|
}
|
|
|
|
@keyframes playIconStop {
|
|
0%,
|
|
100% {
|
|
transform: scale(1);
|
|
}
|
|
|
|
50% {
|
|
transform: scale(1.08);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.play-btn,
|
|
.play-btn::before,
|
|
.play-btn mat-icon,
|
|
.play-btn__icon-shell {
|
|
animation: none !important;
|
|
transition-duration: 1ms !important;
|
|
}
|
|
}
|
|
|
|
.favorite-btn,
|
|
.download-btn {
|
|
@include detail-actions.secondary-action-button;
|
|
}
|
|
|
|
.favorite-btn {
|
|
@include detail-actions.favorite-button-state;
|
|
}
|
|
|
|
.download-btn {
|
|
@include detail-actions.download-button-state;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Content Sections - Below the hero fold
|
|
// ============================================================================
|
|
|
|
.content-section {
|
|
padding: 20px 0;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
|
|
&__title {
|
|
font-size: $season-title-font-size;
|
|
font-weight: $season-title-font-weight;
|
|
letter-spacing: $season-title-letter-spacing;
|
|
margin: 0 0 20px 0;
|
|
color: var(--text-primary);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Seasons & Episodes - Inside details section
|
|
// ============================================================================
|
|
|
|
.seasons {
|
|
margin-top: 24px;
|
|
width: 100%;
|
|
|
|
.season-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 0;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s ease;
|
|
|
|
&:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
h4 {
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.expand-icon {
|
|
font-size: 0.75rem;
|
|
color: var(--text-muted);
|
|
}
|
|
}
|
|
|
|
.episodes {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 10px;
|
|
padding: 8px 0 16px;
|
|
|
|
button {
|
|
margin: 0;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
.episodes-loading,
|
|
.seasons-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 20px 0;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.no-episodes {
|
|
color: var(--text-muted);
|
|
font-style: italic;
|
|
padding: 8px 0;
|
|
}
|
|
}
|
|
|
|
// Seasons loading when inside details
|
|
.details .seasons-loading {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 20px 0;
|
|
color: var(--text-secondary);
|
|
margin-top: 24px;
|
|
}
|
|
|
|
// Season container when inside details section
|
|
.details app-season-container {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 24px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
// ============================================================================
|
|
// YouTube Trailer Section - Inside details
|
|
// ============================================================================
|
|
|
|
.youtube-trailer {
|
|
margin-top: 24px;
|
|
width: 100%;
|
|
|
|
h3 {
|
|
font-size: $episode-title-font-size;
|
|
font-weight: 600;
|
|
letter-spacing: $episode-title-letter-spacing;
|
|
margin: 0 0 12px 0;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.trailer-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 560px;
|
|
aspect-ratio: 16 / 9;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
|
|
|
iframe {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
border: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Legacy container support (for gradual migration)
|
|
// ============================================================================
|
|
|
|
.container {
|
|
display: contents;
|
|
}
|
|
|
|
.image {
|
|
display: none; // Hidden - now using .poster in hero
|
|
}
|
|
|
|
// ============================================================================
|
|
// Mobile Responsive - Option A: Keep poster grid layout
|
|
// ============================================================================
|
|
|
|
@media (max-width: 800px) {
|
|
:host {
|
|
--poster-width: 120px;
|
|
}
|
|
|
|
.action-buttons {
|
|
width: 100%;
|
|
// This class is in ContentHeroComponent now, but these rules apply to children?
|
|
// No, .play-btn is a child of the PROJECTED content.
|
|
// So we should target the buttons directly for mobile
|
|
}
|
|
|
|
@include detail-actions.mobile-action-buttons;
|
|
|
|
.youtube-trailer {
|
|
margin-top: 16px;
|
|
|
|
.trailer-wrapper {
|
|
max-width: 100%;
|
|
border-radius: 4px;
|
|
}
|
|
}
|
|
|
|
.seasons {
|
|
padding: 16px 0 0;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Animations
|
|
// ============================================================================
|
|
|
|
.youtube-trailer {
|
|
animation: fadeInUp 0.5s ease-out 0.4s both;
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Utility - keep .label for backwards compatibility
|
|
// ============================================================================
|
|
|
|
.label {
|
|
font-weight: 400;
|
|
font-size: 0.8125rem;
|
|
color: var(--text-muted);
|
|
text-decoration: none;
|
|
padding: 0;
|
|
opacity: 1;
|
|
}
|
|
}
|