// Shared content grid and card styles // // Choose a relative @use path from each consuming stylesheet. A current // portal-shared component uses its local forwarding module as shown below; // direct consumers of this partial may need a different depth. // @use '../../styles/content-grid' as grid; // @include grid.content-grid; // @include grid.content-card; @mixin content-grid( $min-width: var(--cover-grid-min-width, 148px), $gap: var(--cover-gap, 16px) ) { display: grid; grid-template-columns: repeat(auto-fill, minmax(#{$min-width}, 1fr)); gap: $gap; } @mixin content-card { cursor: pointer; border-radius: 10px; overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; &:hover { transform: translateY(-3px); box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); } } @mixin thumbnail-wrapper { position: relative; border-radius: 10px; overflow: hidden; .poster { width: 100%; aspect-ratio: 2/3; object-fit: cover; display: block; } .poster-placeholder { width: 100%; aspect-ratio: 2/3; display: flex; align-items: center; justify-content: center; background: rgba(128, 128, 128, 0.1); mat-icon { font-size: 36px; width: 36px; height: 36px; opacity: 0.25; } } } @mixin type-badge { position: absolute; top: 6px; left: 6px; padding: 3px 6px; border-radius: 4px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; background: rgba(0, 0, 0, 0.85); color: #fff; &.live { color: #ef5350; } &.radio { color: #ffa726; } &.movie { color: #42a5f5; } &.series { color: #66bb6a; } } @mixin remove-button { position: absolute; top: 4px; right: 4px; z-index: 1; background: rgba(0, 0, 0, 0.5); width: 28px; height: 28px; padding: 0; &:hover { background: rgba(0, 0, 0, 0.8); } mat-icon { color: white; font-size: 18px; width: 18px; height: 18px; } } // Hover/focus caption for the posters-only wall (Settings > General > // "Show titles under covers" off). Placed inside the poster wrapper, which // must be `position: relative` with `overflow: hidden`; the consumer reveals // it on `:hover`/`:focus-visible` and pins it with `--pinned` whenever the // cover cannot identify the item (default poster, placeholder). @mixin cover-title-overlay { position: absolute; inset: auto 0 0 0; box-sizing: border-box; padding: 28px 8px 8px; color: #fff; font-size: 0.78rem; font-weight: 500; line-height: 1.3; text-align: center; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); background: linear-gradient( 180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.72) 50%, rgba(0, 0, 0, 0.88) 100% ); opacity: 0; pointer-events: none; // Entrance under 300ms, ease-out: the caption answers a hover, it is // not a state change worth choreographing. transition: opacity 150ms ease-out; // The clamp lives on a child: `overflow: hidden` clips at the padding // edge, so a clamped element with its own padding paints the third // line into that padding. .cover-title-overlay__text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; word-break: break-word; } &.cover-title-overlay--pinned { opacity: 1; } @media (prefers-reduced-motion: reduce) { transition: none; } } // Draw the ring on the element that owns `overflow: hidden` (or on one of // its ancestors): an outline on a clipped descendant is cut on every edge. @mixin focus-ring-declarations { outline: 2px solid var(--app-selection-color, #2f7bff); outline-offset: 3px; } @mixin card-focus-ring { &:focus-visible { @include focus-ring-declarations; } } @mixin card-info { padding: 8px 10px; h3 { margin: 0; font-size: 0.8rem; font-weight: 500; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .date-info { margin: 4px 0 0; font-size: 0.7rem; opacity: 0.6; } }