mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
627 lines
16 KiB
SCSS
627 lines
16 KiB
SCSS
// ============================================================================
|
|
// Netflix-style Detail View - Cinematic Streaming Design
|
|
// ============================================================================
|
|
|
|
@use '../../../../styles/detail-view-actions' as detail-actions;
|
|
|
|
: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);
|
|
--accent-color: #b81d24;
|
|
--tag-bg: rgba(0, 0, 0, 0.06);
|
|
--tag-border: rgba(0, 0, 0, 0.12);
|
|
|
|
width: 100%;
|
|
display: block;
|
|
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: #e50914;
|
|
--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);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Hero Section - Full container with backdrop
|
|
// ============================================================================
|
|
|
|
.hero {
|
|
position: relative;
|
|
width: 100%;
|
|
// Natural height: the page scroll container lives outside the hero
|
|
// (PortalDetailShellComponent). `overflow: hidden` clips the scaled
|
|
// blurred backdrop fallback.
|
|
overflow: hidden;
|
|
background: var(
|
|
--surface-bg
|
|
); // Solid background prevents any bleed-through
|
|
|
|
// Backdrop image layer - fixed at top, never repeats
|
|
&__backdrop {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: auto; // Only use height, not bottom
|
|
height: 60%;
|
|
background-size: cover;
|
|
background-position: center 20%;
|
|
background-repeat: no-repeat;
|
|
z-index: 0;
|
|
overflow: hidden;
|
|
|
|
&-image {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
object-position: center 20%;
|
|
}
|
|
|
|
// Fallback blur effect when using poster as backdrop
|
|
&--blurred {
|
|
filter: blur(40px) saturate(1.3);
|
|
transform: scale(1.4);
|
|
height: 65%;
|
|
}
|
|
}
|
|
|
|
// Gradient overlays for text legibility
|
|
&__vignette {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0; // Full height to cover everything
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
background: linear-gradient(
|
|
to bottom,
|
|
transparent 0%,
|
|
transparent 35%,
|
|
var(--surface-bg) 55%,
|
|
var(--surface-bg) 100%
|
|
);
|
|
|
|
// Top area gradient for text legibility over backdrop
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 60%;
|
|
background: linear-gradient(
|
|
to top,
|
|
var(--hero-gradient-end) 0%,
|
|
var(--hero-gradient-mid) 50%,
|
|
var(--hero-gradient-start) 100%
|
|
);
|
|
}
|
|
|
|
// Side fade for poster area
|
|
&::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 50%;
|
|
width: 40%;
|
|
background: linear-gradient(
|
|
to right,
|
|
var(--hero-gradient-mid) 0%,
|
|
var(--hero-gradient-start) 100%
|
|
);
|
|
}
|
|
}
|
|
|
|
// Content area (page scroll lives in the surrounding detail shell)
|
|
&__content {
|
|
position: relative;
|
|
z-index: 2;
|
|
display: flex;
|
|
gap: 32px;
|
|
align-items: flex-start;
|
|
padding: 140px 40px 60px;
|
|
// Reserved by the detail shell while its sticky Back control shows.
|
|
padding-inline-start: max(40px, var(--detail-back-lane, 0px));
|
|
max-width: var(--content-max-width);
|
|
margin: 0 auto;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
// Error state
|
|
&__error {
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
gap: 16px;
|
|
min-height: 50vh;
|
|
|
|
.error-icon {
|
|
font-size: 64px;
|
|
width: 64px;
|
|
height: 64px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.error-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
margin: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Poster Card - Floating with shadow
|
|
// ============================================================================
|
|
|
|
.poster {
|
|
flex-shrink: 0;
|
|
width: var(--poster-width);
|
|
align-self: flex-start;
|
|
|
|
&:hover .poster__image {
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
&__image {
|
|
width: 100%;
|
|
aspect-ratio: 2 / 3;
|
|
object-fit: cover;
|
|
border-radius: 8px;
|
|
box-shadow: var(--poster-shadow);
|
|
background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
&__placeholder {
|
|
width: 100%;
|
|
aspect-ratio: 2 / 3;
|
|
border-radius: 8px;
|
|
background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: rgba(255, 255, 255, 0.8);
|
|
box-shadow: var(--poster-shadow);
|
|
overflow: hidden;
|
|
|
|
&-icon {
|
|
font-size: 48px;
|
|
width: 48px;
|
|
height: 48px;
|
|
opacity: 0.5;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
&-text {
|
|
font-size: 1rem;
|
|
font-weight: 500;
|
|
text-align: center;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 4;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
overflow-wrap: anywhere;
|
|
padding: 16px;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Details Section - Title, metadata, description
|
|
// ============================================================================
|
|
|
|
.details {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding-bottom: 24px;
|
|
|
|
&__title {
|
|
font-size: clamp(1.75rem, 4vw, 3rem);
|
|
font-weight: 700;
|
|
line-height: 1.1;
|
|
margin: 0 0 16px 0;
|
|
letter-spacing: -0.02em;
|
|
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
overflow-wrap: anywhere;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
&__tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
// These styles are for Projected Content, we need to ensure they apply
|
|
// using ::ng-deep or just assume the global styles will handle them?
|
|
// Actually, since we project content, the styles defined here might NOT apply to the projected content
|
|
// unless we use ::ng-deep or the content itself has these styles.
|
|
// However, since we are moving the "shell", the container classes (.details__tags) are here.
|
|
// The inner items (.details__tag) will be in the projected content.
|
|
// So we need ::ng-deep here for .details__tag.
|
|
|
|
::ng-deep .details__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);
|
|
}
|
|
|
|
// Rendered as a <button> so the facet is keyboard-operable; the
|
|
// UA's own font and appearance have to be neutralized to keep it
|
|
// visually identical to the informational chips beside it.
|
|
&--clickable {
|
|
appearance: none;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
letter-spacing: inherit;
|
|
line-height: inherit;
|
|
transition:
|
|
border-color 0.15s ease,
|
|
background 0.15s ease;
|
|
|
|
&:hover {
|
|
background: var(--tag-border);
|
|
border-color: var(--text-secondary, rgba(255, 255, 255, 0.5));
|
|
}
|
|
|
|
&:focus-visible {
|
|
outline: 2px solid var(--text-primary, #fff);
|
|
outline-offset: 2px;
|
|
}
|
|
}
|
|
}
|
|
|
|
&__description {
|
|
font-size: 0.9375rem;
|
|
line-height: 1.6;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 6px;
|
|
max-width: 600px;
|
|
|
|
// Truncate long descriptions
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 4;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
|
|
&--expanded {
|
|
display: block;
|
|
-webkit-line-clamp: unset;
|
|
overflow: visible;
|
|
}
|
|
}
|
|
|
|
&__description-toggle {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
margin: 0 0 20px 0;
|
|
padding: 4px 0;
|
|
background: transparent;
|
|
border: 0;
|
|
cursor: pointer;
|
|
color: var(--text-secondary);
|
|
font: inherit;
|
|
font-size: 0.8125rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
transition:
|
|
color 180ms ease,
|
|
transform 180ms ease;
|
|
|
|
mat-icon {
|
|
font-size: 16px;
|
|
width: 16px;
|
|
height: 16px;
|
|
line-height: 16px;
|
|
transition: transform 180ms ease;
|
|
}
|
|
|
|
&:hover {
|
|
color: var(--text-primary);
|
|
|
|
mat-icon {
|
|
transform: translateY(2px);
|
|
}
|
|
}
|
|
|
|
&[aria-expanded='true']:hover mat-icon {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
&:focus-visible {
|
|
outline: 2px solid var(--accent-color);
|
|
outline-offset: 2px;
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
|
|
&__meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 16px 32px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
// Similar issue for meta items
|
|
::ng-deep .details__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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Action Buttons - Play, Favorite
|
|
// ============================================================================
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
margin-top: 0;
|
|
|
|
// Use ::ng-deep to style the projected buttons
|
|
::ng-deep .play-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-height: 64px;
|
|
padding: 10px 24px 10px 18px;
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
background: #fff;
|
|
color: #141414;
|
|
border: none;
|
|
border-radius: 16px;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
|
|
&:hover {
|
|
background: rgba(255, 255, 255, 0.85);
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
mat-icon {
|
|
font-size: 24px;
|
|
width: 24px;
|
|
height: 24px;
|
|
}
|
|
|
|
@include detail-actions.play-button-copy;
|
|
}
|
|
|
|
::ng-deep .favorite-btn,
|
|
::ng-deep .download-btn,
|
|
// The sources chip is a peer secondary action, so it wears the same
|
|
// surface, height and radius. Without this it reads as a link floating
|
|
// between three solid buttons.
|
|
::ng-deep app-vod-sources-chip .sources-chip {
|
|
@include detail-actions.secondary-action-button;
|
|
}
|
|
|
|
// Match the chip to its neighbouring icon buttons' tag surfaces. Its own
|
|
// pressed state (.sources-chip--open) is more specific and still wins.
|
|
::ng-deep app-vod-sources-chip .sources-chip {
|
|
background: var(--tag-bg);
|
|
border-color: var(--tag-border);
|
|
|
|
&:hover {
|
|
background: var(--tag-border);
|
|
}
|
|
}
|
|
|
|
::ng-deep .favorite-btn {
|
|
@include detail-actions.favorite-button-state;
|
|
}
|
|
|
|
::ng-deep .download-btn {
|
|
@include detail-actions.download-button-state;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Mobile Responsive - Option A: Keep poster grid layout
|
|
// ============================================================================
|
|
|
|
@media (max-width: 800px) {
|
|
:host {
|
|
--poster-width: 120px;
|
|
}
|
|
|
|
.hero {
|
|
&__backdrop {
|
|
height: 45%;
|
|
|
|
&--blurred {
|
|
height: 50%;
|
|
}
|
|
}
|
|
|
|
&__vignette {
|
|
background: linear-gradient(
|
|
to bottom,
|
|
transparent 0%,
|
|
transparent 25%,
|
|
var(--surface-bg) 45%,
|
|
var(--surface-bg) 100%
|
|
);
|
|
|
|
&::before {
|
|
height: 50%;
|
|
}
|
|
|
|
&::after {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
&__content {
|
|
padding: 60px 20px 40px;
|
|
padding-inline-start: max(20px, var(--detail-back-lane, 0px));
|
|
gap: 16px;
|
|
}
|
|
}
|
|
|
|
.poster {
|
|
// Mobile poster adjustments - none needed
|
|
}
|
|
|
|
.details {
|
|
&__title {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
&__description {
|
|
-webkit-line-clamp: 3;
|
|
|
|
&--expanded {
|
|
-webkit-line-clamp: unset;
|
|
}
|
|
}
|
|
|
|
&__meta {
|
|
gap: 12px 20px;
|
|
}
|
|
}
|
|
|
|
.action-buttons {
|
|
width: 100%;
|
|
|
|
@include detail-actions.projected-mobile-action-buttons;
|
|
}
|
|
}
|
|
|
|
// A phone cannot carry the poster and the details side by side: the details
|
|
// column drops to ~200px, and the action buttons are then squeezed below
|
|
// their own labels ("Play" clipped down to its icon). Stack them instead.
|
|
@media (max-width: 640px) {
|
|
.hero__content {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
padding-inline: 16px;
|
|
padding-inline-start: max(16px, var(--detail-back-lane, 0px));
|
|
}
|
|
|
|
.poster {
|
|
width: 140px;
|
|
align-self: center;
|
|
}
|
|
|
|
.details__title {
|
|
text-align: center;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Animations
|
|
// ============================================================================
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.hero__content {
|
|
animation: fadeInUp 0.6s ease-out 0.2s both;
|
|
}
|
|
|
|
// Skeleton Loader Overrides
|
|
ngx-skeleton-loader {
|
|
display: block;
|
|
width: 100%;
|
|
|
|
// Ensure the loader inside poster takes up the correct aspect ratio space
|
|
::ng-deep .loader {
|
|
display: block;
|
|
height: 100%;
|
|
}
|
|
}
|
|
|
|
:host-context(.dark-theme) {
|
|
ngx-skeleton-loader {
|
|
::ng-deep .loader {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
}
|
|
}
|
|
|
|
.poster {
|
|
// ... existing styles ...
|
|
// Ensure poster container has height/ratio for skeleton
|
|
min-height: 330px; // Fallback
|
|
aspect-ratio: 2 / 3;
|
|
}
|
|
|
|
// After the rule above so it wins the cascade at equal specificity: that
|
|
// 330px skeleton fallback is sized for the desktop poster width, while the
|
|
// stacked phone hero renders the poster 140px wide — its aspect-ratio height
|
|
// is ~210px, and the leftover minimum was pure empty space pushing the title
|
|
// and actions ~120px down on every loaded detail page.
|
|
@media (max-width: 640px) {
|
|
.poster {
|
|
min-height: 0;
|
|
}
|
|
}
|