Files
jacuzzi/frontend/css/style.css
Simon 2abdc31f56 Put the reel itself in the picture-in-picture window, so it scrolls
A video picture-in-picture window renders one <video>'s frames and nothing
else, which is why moving through the reel from it had to be smuggled in
through media keys. Document picture-in-picture opens a real document
instead, so the feed is moved into it: #feed-view and its whole subtree are
appended to the new window's body. Nothing is copied and no <video> is
re-created, so playback continues across the move and the scroll-snap list,
the panes, the split tree and every control keep working -- they are the same
elements, in another window. Scrolling the window scrolls the reel, because
it is the reel.

The lookups in feed.js now go through the cached #feed-view root rather than
document.getElementById, since after the move the feed is no longer in this
document, and the HUD-idle class rides on that root so it travels with it.
The page underneath drops back to the grid while the feed is out, and gets it
back when the window closes.

The video window stays as the fallback: it is Chrome-only, needs a user
gesture, and a hidden tab has none -- so auto-PiP on tab switch is still the
old path. It is suppressed while a document window is open, or the browser
would tear the video out of it on the next switch.

Also fixes the picture-in-picture button, which referenced an undefined
`pane` in bindSharedControls (the parameter was named `slide`) and threw into
a swallowed promise rejection on every click.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
2026-09-10 20:45:49 +00:00

2728 lines
58 KiB
CSS

* { margin: 0; padding: 0; box-sizing: border-box; }
/* A card keeps its optional parts (live badge, uploader, duration, tags) at all
times and hides the ones this video doesn't need, so any pooled card fits any
video -- see bindCard. Several of those carry their own `display`, which beats
the UA rule for [hidden], so say it once and mean it. */
[hidden] { display: none !important; }
:root {
/* Warm "classic" dark: deep charcoal with a hint of brown, never pure black. */
--bg-primary: #14110d;
--bg-secondary: rgba(28, 24, 18, 0.86);
--bg-tertiary: rgba(244, 232, 212, 0.06);
--text-primary: #f4ece0;
--text-secondary: rgba(244, 236, 224, 0.62);
/* Modern brass accent — the single chromatic note against the neutrals. */
--accent: #c9a567;
--accent-hover: #d9b87f;
--border: rgba(244, 232, 212, 0.12);
--border-hover: rgba(201, 165, 103, 0.45);
--shadow: rgba(8, 6, 3, 0.5);
--font-body: 'Sora', 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
--font-display: 'Fraunces', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif;
--font-logo: 'Fraunces', 'Iowan Old Style', 'Palatino Linotype', Georgia, serif;
--radius-lg: 14px;
--radius-md: 10px;
--radius-sm: 7px;
--grid-max: 1480px;
--focus-ring: rgba(201, 165, 103, 0.32);
color-scheme: dark;
}
html, body { height: 100%; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background:
radial-gradient(1100px 560px at 14% -12%, rgba(201, 165, 103, 0.10), transparent 60%),
radial-gradient(900px 520px at 108% 4%, rgba(201, 165, 103, 0.06), transparent 58%),
var(--bg-primary);
background-attachment: fixed;
color: var(--text-primary);
font-family: var(--font-body);
overflow-x: hidden;
font-size: 14px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
/* No accidental zoom: `manipulation` drops double-tap-to-zoom (the app's own
taps are handled in JS anyway) while leaving panning and deliberate
pinch-zoom alone, and text-size-adjust stops Safari inflating text of its
own accord after a rotation. */
touch-action: manipulation;
-webkit-text-size-adjust: 100%;
text-size-adjust: 100%;
}
body.drawer-open {
overflow: hidden;
}
/* Subtle film grain so large flat surfaces don't read as sterile vector flats. */
body::before {
content: '';
position: fixed;
inset: 0;
z-index: 50;
pointer-events: none;
opacity: 0.035;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
body.theme-light::before {
opacity: 0.05;
mix-blend-mode: multiply;
}
body:not(.theme-light) {
color-scheme: dark;
}
body.theme-light {
/* Warm ivory "paper" with a warm-ink text — editorial, not stark white/black. */
--bg-primary: #f6f1e7;
--bg-secondary: rgba(255, 252, 245, 0.86);
--bg-tertiary: rgba(43, 33, 18, 0.05);
--text-primary: #241d12;
--text-secondary: rgba(36, 29, 18, 0.62);
--accent: #9a7338;
--accent-hover: #835f2c;
--border: rgba(43, 33, 18, 0.12);
--border-hover: rgba(154, 115, 56, 0.5);
--shadow: rgba(74, 56, 28, 0.14);
--focus-ring: rgba(154, 115, 56, 0.28);
color-scheme: light;
background:
radial-gradient(1100px 560px at 14% -12%, rgba(154, 115, 56, 0.08), transparent 60%),
radial-gradient(900px 520px at 108% 4%, rgba(154, 115, 56, 0.05), transparent 58%),
var(--bg-primary);
}
/* Top Bar */
.top-bar {
height: 64px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 clamp(16px, 3vw, 36px);
background: linear-gradient(180deg, rgba(20, 17, 13, 0.86), rgba(20, 17, 13, 0.62));
position: sticky;
top: 0;
z-index: 100;
border-bottom: 1px solid var(--border);
gap: 24px;
backdrop-filter: blur(12px);
box-shadow: 0 8px 24px var(--shadow);
}
body.theme-light .top-bar {
background: linear-gradient(180deg, rgba(246, 241, 231, 0.94), rgba(246, 241, 231, 0.78));
}
.logo {
font-size: 28px;
font-weight: 500;
letter-spacing: -0.01em;
color: var(--text-primary);
font-family: var(--font-logo);
font-optical-sizing: auto;
flex-shrink: 0;
}
.search-container {
flex: 1;
max-width: 500px;
position: relative;
}
.search-container input {
width: 100%;
padding: 10px 44px 10px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 999px;
color: var(--text-primary);
font-size: 14px;
transition: all 0.2s ease;
}
.search-container input:focus {
outline: none;
border-color: var(--accent);
background: var(--bg-tertiary);
box-shadow: 0 0 0 4px var(--focus-ring);
}
.search-container input::placeholder {
color: var(--text-secondary);
}
.search-clear-btn {
position: absolute;
top: 50%;
right: 12px;
transform: translateY(-50%);
width: 26px;
height: 26px;
border: none;
border-radius: 50%;
background: var(--bg-secondary);
color: var(--text-secondary);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
line-height: 1;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, background 0.2s ease, color 0.2s ease;
}
.search-clear-btn.is-visible {
opacity: 1;
pointer-events: auto;
}
.search-clear-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.actions {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.icon-btn {
width: 48px;
height: 48px;
border: none;
background: transparent;
border-radius: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
transition: all 0.2s ease;
font-size: 20px;
}
.icon-btn:hover {
background: var(--bg-tertiary);
color: var(--accent);
}
.icon-btn:active {
transform: scale(0.92);
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* CDN-served icon images (Heroicons). Black SVG sources, tinted via filter:
neutral to match text by default, brass on hover for the accent moment. */
.icon-svg {
width: 24px;
height: 24px;
display: block;
filter: invert(92%) sepia(6%) saturate(220%) hue-rotate(2deg) brightness(101%);
}
/* Brass tint approximated from a pure-black source. */
.icon-btn:hover .icon-svg {
filter: invert(68%) sepia(42%) saturate(560%) hue-rotate(1deg) brightness(94%) contrast(86%);
}
body.theme-light .icon-svg {
filter: invert(12%) sepia(20%) saturate(620%) hue-rotate(2deg) brightness(95%);
}
body.theme-light .icon-btn:hover .icon-svg {
filter: invert(46%) sepia(36%) saturate(720%) hue-rotate(1deg) brightness(82%) contrast(88%);
}
/* Hamburger Menu */
.hamburger {
display: flex;
flex-direction: column;
gap: 4px;
}
.hamburger span {
width: 24px;
height: 2px;
background: currentColor;
border-radius: 1px;
transition: all 0.3s ease;
}
.menu-toggle.active .hamburger span:nth-child(1) {
transform: translateY(6px) rotate(45deg);
}
.menu-toggle.active .hamburger span:nth-child(2) {
opacity: 0;
}
.menu-toggle.active .hamburger span:nth-child(3) {
transform: translateY(-6px) rotate(-45deg);
}
/* Sidebar */
.sidebar {
position: fixed;
top: 0;
right: -100%;
width: 320px;
height: 100vh;
background: var(--bg-secondary);
z-index: 1001;
transition: right 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
backdrop-filter: blur(15px);
box-shadow: -20px 0 40px var(--shadow);
}
body.theme-light .sidebar {
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
}
.sidebar.open {
right: 0;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--border);
}
.sidebar-header h3 {
font-size: 18px;
font-weight: 600;
font-family: var(--font-display);
}
.close-btn {
background: none;
border: none;
color: var(--text-primary);
cursor: pointer;
font-size: 24px;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
}
.close-btn:hover {
color: var(--accent);
}
.sidebar-content {
flex: 1;
padding: 12px 0;
overflow-y: auto;
}
.sidebar-section {
padding: 8px 0 4px 0;
border-bottom: 1px solid var(--border);
}
.sidebar-section:last-child {
border-bottom: none;
}
.sidebar-subtitle {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 8px 24px 4px 24px;
}
.sidebar-item {
display: block;
padding: 12px 24px;
color: var(--text-primary);
text-decoration: none;
transition: all 0.2s ease;
border-left: 3px solid transparent;
}
.sidebar-item:hover {
background: var(--bg-tertiary);
border-left-color: var(--accent);
color: var(--accent);
}
.setting-item {
padding: 16px 24px;
border-bottom: 1px solid var(--border);
}
.filters-container .setting-item {
padding-top: 12px;
padding-bottom: 12px;
}
.filters-empty {
padding: 12px 24px 20px 24px;
color: var(--text-secondary);
font-size: 13px;
}
.setting-item label {
display: block;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.setting-label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
/* Explanatory line under a control -- and where the import reports back. Not a
`label`, so it keeps sentence case and normal letter spacing. */
.setting-note {
margin: 8px 0 0;
font-size: 12px;
line-height: 1.45;
color: var(--text-secondary);
}
.setting-label-row label {
margin-bottom: 0;
}
.setting-toggle .setting-label-row {
margin-bottom: 10px;
}
.toggle {
position: relative;
display: inline-flex;
align-items: center;
width: 46px;
height: 26px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-track {
position: absolute;
inset: 0;
border-radius: 999px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
transition: all 0.2s ease;
}
.toggle-track::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--text-primary);
transition: transform 0.2s ease, background 0.2s ease;
box-shadow: 0 4px 10px var(--shadow);
}
.toggle input:checked + .toggle-track {
background: var(--accent);
border-color: var(--accent);
}
.toggle input:checked + .toggle-track::after {
transform: translateX(20px);
background: var(--bg-primary);
}
.toggle input:focus-visible + .toggle-track {
box-shadow: 0 0 0 4px var(--focus-ring);
}
.btn-link {
background: transparent;
border: none;
color: var(--text-secondary);
font-size: 12px;
cursor: pointer;
text-decoration: underline;
padding: 0;
}
.btn-link:hover {
color: var(--text-primary);
}
.btn-link:disabled {
color: var(--border);
cursor: not-allowed;
text-decoration: none;
}
.input-row {
display: flex;
gap: 8px;
align-items: center;
}
.input-row input {
flex: 1;
padding: 9px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
font-size: 14px;
}
.input-row input::placeholder {
color: var(--text-secondary);
}
.input-row input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--focus-ring);
}
body.theme-light .input-row input:focus {
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.08);
}
.btn-secondary {
padding: 9px 14px;
border-radius: 999px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 13px;
transition: all 0.2s ease;
width: 100%;
text-align: center;
}
.btn-secondary:hover {
background: var(--bg-tertiary);
border-color: var(--border-hover);
}
.btn-secondary:active {
transform: translateY(1px);
}
.sources-list {
padding: 8px 24px 16px 24px;
display: flex;
flex-direction: column;
gap: 8px;
}
.source-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-tertiary);
}
.source-item span {
font-size: 12px;
color: var(--text-primary);
word-break: break-all;
}
.source-item button {
padding: 6px 10px;
border-radius: 999px;
border: 1px solid var(--border);
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 12px;
}
.source-item button:hover {
background: var(--bg-tertiary);
}
.setting-item input[type="range"] {
width: 100%;
appearance: none;
-webkit-appearance: none;
height: 6px;
border-radius: 999px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
cursor: pointer;
margin: 6px 0;
}
.setting-item input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--text-primary);
border: none;
cursor: pointer;
}
.setting-item input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--text-primary);
border: none;
cursor: pointer;
}
.setting-item select {
width: 100%;
padding: 9px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
cursor: pointer;
font-size: 14px;
color-scheme: dark;
appearance: none;
background-image:
linear-gradient(45deg, transparent 50%, var(--text-secondary) 50%),
linear-gradient(135deg, var(--text-secondary) 50%, transparent 50%),
linear-gradient(to right, transparent, transparent);
background-position:
calc(100% - 16px) calc(1em + 1px),
calc(100% - 11px) calc(1em + 1px),
100% 0;
background-size:
5px 5px,
5px 5px,
2.5em 2.5em;
background-repeat: no-repeat;
}
.multi-select {
display: flex;
flex-direction: column;
gap: 8px;
}
.multi-select-item {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: var(--text-primary);
padding: 6px 10px;
border-radius: 8px;
border: 1px solid transparent;
background: transparent;
transition: all 0.2s ease;
}
.multi-select-item input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--accent);
flex-shrink: 0;
}
.multi-select-item span {
padding-left: 6px;
}
.multi-select-item:hover {
border-color: var(--border);
background: var(--bg-tertiary);
}
.multi-select-item input[type="checkbox"]:checked + span {
color: var(--accent);
}
.setting-item select:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--focus-ring);
}
body.theme-light .setting-item select:focus {
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.08);
}
.setting-item select,
.setting-item select option,
.setting-item select optgroup {
background-color: #0a0a0a;
color: #ffffff;
}
body.theme-light .setting-item select {
color-scheme: light;
}
body.theme-light .setting-item select,
body.theme-light .setting-item select option,
body.theme-light .setting-item select optgroup {
background-color: #ffffff;
color: #000000;
}
.setting-item select option {
background: #0a0a0a;
color: #ffffff;
}
body.theme-light .setting-item select option {
background: #ffffff;
color: #000000;
}
/* Overlay */
.sidebar-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: none;
z-index: 1000;
transition: opacity 0.3s ease;
backdrop-filter: blur(2px);
}
.sidebar-overlay.open {
display: block;
}
.load-more-btn {
position: sticky;
left: 50%;
transform: translateX(-50%);
margin: 16px auto 32px auto;
width: 52px;
height: 52px;
border-radius: 999px;
border: 1px solid var(--border);
background: var(--bg-secondary);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
box-shadow: 0 10px 20px var(--shadow);
}
.load-more-btn:hover {
background: var(--bg-tertiary);
}
.load-more-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Favorites Bar */
.favorites-bar {
padding: 16px clamp(16px, 3vw, 36px) 4px;
max-width: var(--grid-max);
margin: 0 auto;
}
.favorites-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.favorites-header h3 {
font-size: 12px;
letter-spacing: 1px;
text-transform: uppercase;
color: var(--text-secondary);
font-family: var(--font-display);
}
.favorites-actions {
display: flex;
align-items: center;
gap: 8px;
}
.favorites-sort {
height: 28px;
padding: 0 8px;
border-radius: var(--radius-sm, 6px);
border: 1px solid var(--border);
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 12px;
}
.favorites-actions .btn-secondary {
height: 28px;
padding: 0 12px;
font-size: 12px;
}
/* Browsing favorites as a grid: the bar above it would be the same list twice,
so it collapses to its header (which carries the way back out). */
body.favorites-view-open .favorites-list,
body.favorites-view-open .favorites-empty {
display: none;
}
.favorites-list {
display: flex;
gap: 12px;
overflow-x: auto;
padding-bottom: 12px;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.favorites-list::-webkit-scrollbar {
height: 8px;
}
.favorites-list::-webkit-scrollbar-thumb {
background: var(--bg-tertiary);
border-radius: 999px;
}
.favorites-list::-webkit-scrollbar-track {
background: transparent;
}
.favorite-card {
position: relative;
flex: 0 0 auto;
width: 220px;
border-radius: var(--radius-md);
border: 1px solid var(--border);
background: var(--bg-secondary);
overflow: hidden;
cursor: pointer;
box-shadow: 0 8px 18px var(--shadow);
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
scroll-snap-align: start;
}
.favorite-card:hover {
transform: translateY(-4px);
box-shadow: 0 14px 24px var(--shadow);
border-color: var(--border-hover);
}
.favorite-card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
background: var(--bg-tertiary);
}
.favorite-info {
padding: 10px 12px 12px 12px;
}
/* One line, always. A title too long for the card scrolls across it (see
App.marquee) rather than wrapping the card to an uneven height. */
.favorite-info h4 {
font-size: 13px;
font-weight: 600;
margin: 0;
color: var(--text-primary);
font-family: var(--font-display);
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
position: relative;
}
.favorite-title-text {
display: inline-block;
padding-right: 24px;
transform: translateX(0);
}
.favorite-card.is-title-active .favorite-title-text {
animation: video-title-marquee var(--marquee-duration, 10s) linear infinite;
will-change: transform;
}
.favorites-empty {
font-size: 13px;
color: var(--text-secondary);
padding: 4px 0 12px 0;
}
/* Grid Container */
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
grid-auto-rows: 10px;
gap: 16px;
align-items: start;
padding: 24px;
max-width: var(--grid-max);
margin: 0 auto;
}
@media (max-width: 768px) {
.grid-container {
/* Exactly two larger cards per row on phones, rather than packing in
several small ones. */
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 10px;
gap: 12px;
padding: 16px;
}
.top-bar {
flex-wrap: wrap;
height: auto;
padding: 12px 16px;
row-gap: 10px;
}
.search-container {
order: 3;
width: 100%;
max-width: 100%;
flex-basis: 100%;
}
.actions {
order: 2;
width: auto;
margin-left: auto;
justify-content: flex-end;
}
}
@media (max-width: 480px) {
.grid-container {
/* One full-width card per row on phones. */
grid-template-columns: 1fr;
gap: 16px;
}
.logo {
font-size: 18px;
}
.icon-btn {
width: 44px;
height: 44px;
}
}
@media (min-width: 1600px) {
body {
font-size: 16px;
}
.top-bar {
height: 72px;
padding: 0 36px;
}
.grid-container {
gap: 24px;
padding: 32px 48px;
}
.video-card h4 {
font-size: calc(16px * var(--card-font-scale, 1));
}
.video-card p {
font-size: calc(13px * var(--card-font-scale, 1));
}
}
@media (min-width: 1920px) {
.grid-container {
column-width: 280px;
}
}
@media (pointer: coarse) {
.icon-btn {
width: 52px;
height: 52px;
}
.btn-secondary {
padding: 10px 14px;
}
.setting-item select,
.input-row input {
padding: 10px 14px;
}
/* iOS zooms the whole page when you focus a control whose text is under
16px, and it ignores user-scalable=no -- so the font size is the only
lever that actually stops it. Every text control gets 16px on touch
devices; the padding above keeps them looking the same size. */
.search-container input,
.input-row input,
.setting-item select,
.cmdk-input,
input[type="text"],
input[type="search"],
input[type="number"],
input[type="url"],
input[type="email"],
input[type="password"],
input:not([type]),
textarea,
select {
font-size: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
transition: none !important;
animation: none !important;
}
}
/* Video Card */
.video-card {
cursor: pointer;
border-radius: var(--radius-lg);
overflow: hidden;
transition: transform 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
background: var(--bg-secondary);
display: inline-flex;
flex-direction: column;
width: 100%;
border: 1px solid var(--border);
box-shadow: 0 6px 16px var(--shadow);
position: relative;
margin-bottom: 0;
/* The grid is virtualized in JS (see App.virtualGrid): only cards near the
viewport are in the DOM at all, and each is absolutely positioned at a
precomputed (top,left). */
}
/* Each card's layout and paint stay its own business, so inserting one during
a scroll cannot make the browser reconsider the rest of the grid. */
.video-card {
contain: layout paint;
}
.video-card:hover {
transform: translateY(-6px);
box-shadow: 0 16px 28px var(--shadow);
border-color: var(--border-hover);
}
.video-thumb {
position: relative;
display: block;
}
.video-card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
display: block;
background: var(--bg-tertiary);
}
/* Dark-transparent pills overlaid on the bottom of the thumbnail.
Scoped under .video-thumb so they outrank the later .uploader-link rule. */
.video-thumb .video-uploader,
.video-thumb .video-duration {
position: absolute;
bottom: 8px;
z-index: 3;
padding: 3px 8px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 12px;
font-weight: 500;
line-height: 1.2;
backdrop-filter: blur(2px);
pointer-events: none;
}
.video-thumb .video-duration {
right: 8px;
}
.video-thumb .video-uploader {
left: 8px;
max-width: 60%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border: none;
text-align: left;
font-family: inherit;
cursor: pointer;
pointer-events: auto;
}
.video-thumb .video-uploader:hover {
background: rgba(0, 0, 0, 0.78);
color: #fff;
text-decoration: underline;
}
.video-card h4 {
font-size: calc(15px * var(--card-font-scale, 1));
font-weight: 500;
padding: 12px 12px 10px;
line-height: 1.3;
color: var(--text-primary);
margin: 0;
flex: 1;
font-family: var(--font-display);
font-optical-sizing: auto;
letter-spacing: -0.01em;
white-space: nowrap;
overflow: hidden;
position: relative;
}
.video-title-text {
display: inline-block;
padding-right: 24px;
transform: translateX(0);
}
.video-card.is-title-active .video-title-text {
animation: video-title-marquee var(--marquee-duration, 10s) linear infinite;
will-change: transform;
}
@keyframes video-title-marquee {
to {
transform: translateX(calc(-1 * var(--marquee-distance, 0px)));
}
}
.video-card p {
font-size: calc(12px * var(--card-font-scale, 1));
color: var(--text-secondary);
padding: 0 12px 12px 12px;
margin: 0;
}
.video-tags {
display: flex;
flex-wrap: nowrap;
gap: 6px;
padding: 0 12px 8px 12px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.video-tag {
font-size: calc(11px * var(--card-font-scale, 1));
color: var(--text-secondary);
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 999px;
padding: 3px 8px;
line-height: 1.2;
letter-spacing: 0.2px;
white-space: nowrap;
font-family: inherit;
cursor: pointer;
}
.video-tag:focus-visible {
outline: 2px solid var(--text-primary);
outline-offset: 1px;
}
.uploader-link {
background: transparent;
border: none;
color: inherit;
font: inherit;
cursor: pointer;
padding: 0;
text-align: left;
}
.video-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
opacity: 0;
transition: opacity 0.2s ease;
pointer-events: none;
z-index: 4;
}
.video-card.is-loading .video-loading,
.favorite-card.is-loading .video-loading {
opacity: 1;
}
.video-loading-spinner {
width: 34px;
height: 34px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.35);
border-top-color: var(--text-primary);
animation: video-card-spinner 0.8s linear infinite;
}
@keyframes video-card-spinner {
to {
transform: rotate(360deg);
}
}
.uploader-link:hover {
color: var(--text-primary);
text-decoration: underline;
}
.video-menu-btn {
position: absolute;
top: 10px;
right: 10px;
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.55);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
line-height: 1;
cursor: pointer;
z-index: 2;
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
body.theme-light .video-menu-btn {
background: rgba(255, 255, 255, 0.8);
}
.video-menu-btn:hover {
transform: scale(1.05);
background: var(--bg-secondary);
}
.video-menu {
position: absolute;
top: 48px;
right: 10px;
min-width: 140px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg-secondary);
box-shadow: 0 12px 24px var(--shadow);
opacity: 0;
transform: translateY(-6px);
pointer-events: none;
transition: opacity 0.18s ease, transform 0.18s ease;
z-index: 3;
}
.video-menu.open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.video-menu-item {
width: 100%;
padding: 10px 12px;
text-align: left;
border: none;
background: transparent;
color: var(--text-primary);
cursor: pointer;
font-size: 13px;
}
.video-menu-item:hover {
background: var(--bg-tertiary);
}
.live-badge {
position: absolute;
top: 10px;
left: 50px;
z-index: 2;
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
border-radius: 999px;
background: #e0245e;
color: #fff;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
line-height: 1;
text-transform: uppercase;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
pointer-events: none;
}
.feed-live-badge {
top: 16px;
left: 16px;
}
/* Live streams can't be seeked, so hide the feed scrubber. */
.feed-pane.is-live .feed-timeline {
display: none;
}
.favorite-btn {
position: absolute;
top: 10px;
left: 10px;
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.55);
color: var(--text-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
line-height: 1;
cursor: pointer;
z-index: 1;
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
body.theme-light .favorite-btn {
background: rgba(255, 255, 255, 0.8);
}
.favorite-btn:hover {
transform: scale(1.05);
background: var(--bg-secondary);
}
.favorite-btn.is-favorite {
color: #ff3b30;
border-color: rgba(255, 59, 48, 0.6);
background: rgba(255, 59, 48, 0.12);
}
/* Info Modal */
.info-modal {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
z-index: 2500;
padding: 20px;
}
.info-modal.open {
display: flex;
}
.info-card {
position: relative;
width: min(520px, 92vw);
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 20px 22px;
box-shadow: 0 18px 36px var(--shadow);
}
.info-card h3 {
font-family: var(--font-display);
font-size: 18px;
margin: 0 0 16px 0;
}
.info-close {
position: absolute;
right: 16px;
top: 16px;
border: none;
background: var(--bg-tertiary);
color: var(--text-primary);
border-radius: 50%;
width: 32px;
height: 32px;
cursor: pointer;
}
.info-rows,
.info-list {
display: flex;
flex-direction: column;
gap: 10px;
}
/* The panel shows every field the client has, which for a resolved video is the
extractor's whole payload -- dozens of rows. Give the list its own scroll so
the card stays inside the viewport and the close button stays put. */
.info-list {
max-height: min(70vh, 620px);
overflow-y: auto;
padding-right: 4px;
}
.info-section {
font-family: var(--font-display);
text-transform: uppercase;
letter-spacing: 0.8px;
font-size: 11px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
padding: 6px 0;
}
.info-pending {
font-size: 12px;
color: var(--text-secondary);
padding-top: 4px;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
font-size: 13px;
color: var(--text-secondary);
border-bottom: 1px solid var(--border);
padding-bottom: 8px;
}
.info-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.info-label {
text-transform: uppercase;
letter-spacing: 0.6px;
font-size: 11px;
color: var(--text-secondary);
}
.info-value {
text-align: right;
color: var(--text-primary);
word-break: break-all;
}
.info-json {
margin: 0;
white-space: pre-wrap;
text-align: left;
color: var(--text-primary);
background: var(--bg-tertiary);
border-radius: 8px;
padding: 8px 10px;
font-size: 12px;
line-height: 1.4;
max-height: 180px;
overflow: auto;
flex: 1;
}
.info-empty {
font-size: 13px;
color: var(--text-secondary);
padding-top: 6px;
}
/* --- Custom player: single fake-fullscreen state everywhere ------------ */
.custom-player {
display: none;
position: fixed;
inset: 0;
background: #000;
z-index: 3000;
touch-action: none;
transition: transform 0.25s ease, opacity 0.25s ease;
}
.custom-player.open {
display: block;
animation: cp-open 0.22s ease;
}
@keyframes cp-open {
from { opacity: 0; }
to { opacity: 1; }
}
.cp-surface {
position: absolute;
inset: 0;
}
/* Ambient backdrop: a blurred copy of the poster fills any letterbox bars
behind the contained video (set via the --poster custom property). */
.cp-surface::before {
content: '';
position: absolute;
inset: 0;
background-image: var(--poster);
background-size: cover;
background-position: center;
filter: blur(60px) saturate(1.3) brightness(0.5);
transform: scale(1.25);
opacity: 0.55;
z-index: 0;
pointer-events: none;
}
.cp-video {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: contain;
background: #000;
}
.cp-flash {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 8px 18px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: 16px;
font-weight: 500;
letter-spacing: 0.02em;
pointer-events: none;
opacity: 0;
z-index: 5;
}
.cp-flash.is-visible {
animation: cp-flash-fade 0.7s ease forwards;
}
@keyframes cp-flash-fade {
0% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
100% { opacity: 0; transform: translate(-50%, -50%) scale(1.08); }
}
.cp-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
pointer-events: none;
transition: opacity 0.15s ease;
z-index: 4;
}
.cp-spinner.is-visible {
opacity: 1;
}
.cp-spinner-ring {
width: 44px;
height: 44px;
border-radius: 50%;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: var(--accent);
animation: cp-spin 0.8s linear infinite;
}
@keyframes cp-spin {
to { transform: rotate(360deg); }
}
.cp-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 20px 24px;
background: rgba(0, 0, 0, 0.7);
border-radius: var(--radius-lg);
color: #fff;
text-align: center;
max-width: min(360px, 80vw);
z-index: 5;
}
.cp-error-actions {
display: flex;
gap: 10px;
}
.cp-retry-btn,
.cp-open-btn {
padding: 8px 20px;
border-radius: 999px;
border: 1px solid var(--accent);
background: transparent;
color: var(--accent);
cursor: pointer;
font: inherit;
text-decoration: none;
line-height: 1.2;
}
.cp-retry-btn:hover,
.cp-open-btn:hover {
background: rgba(201, 165, 103, 0.15);
}
.cp-open-btn[hidden] {
display: none;
}
.cp-replay-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 64px;
height: 64px;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 5;
}
.cp-replay-btn .icon-svg {
width: 30px;
height: 30px;
filter: invert(1);
}
.cp-hud {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
pointer-events: none;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), transparent 18%, transparent 78%, rgba(0, 0, 0, 0.6));
opacity: 1;
transition: opacity 0.3s ease;
z-index: 3;
}
.custom-player.cp-hud-idle .cp-hud {
opacity: 0;
}
.cp-hud * {
pointer-events: auto;
}
.cp-top-bar {
display: flex;
align-items: center;
gap: 12px;
padding: max(14px, env(safe-area-inset-top)) 16px 0;
}
.cp-close-btn {
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 50%;
border: none;
background: rgba(0, 0, 0, 0.5);
color: #fff;
font-size: 18px;
cursor: pointer;
}
.cp-title {
flex: 1;
min-width: 0;
margin: 0;
font-family: var(--font-display);
font-size: 16px;
color: #fff;
white-space: nowrap;
overflow: hidden;
}
.cp-title-text {
display: inline-block;
padding-right: 24px;
transform: translateX(0);
}
.cp-title.has-marquee .cp-title-text {
animation: video-title-marquee var(--marquee-duration, 10s) linear infinite;
}
.cp-fav-btn {
position: static;
flex-shrink: 0;
}
.cp-bottom-bar {
padding: 0 16px max(14px, env(safe-area-inset-bottom));
}
.custom-player.is-live .cp-timeline,
.custom-player.is-live .cp-skip-back-btn,
.custom-player.is-live .cp-skip-fwd-btn {
display: none;
}
.cp-timeline {
padding: 10px 0;
cursor: pointer;
}
.cp-timeline-track {
position: relative;
height: 4px;
border-radius: 2px;
background: rgba(255, 255, 255, 0.25);
}
.cp-timeline-buffered {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0%;
border-radius: 2px;
background: rgba(255, 255, 255, 0.35);
}
.cp-timeline-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 0%;
border-radius: 2px;
background: var(--accent);
}
.cp-timeline-handle {
position: absolute;
top: 50%;
left: 0%;
width: 13px;
height: 13px;
border-radius: 50%;
background: var(--accent);
transform: translate(-50%, -50%) scale(0.7);
transition: transform 0.15s ease;
}
.cp-timeline:hover .cp-timeline-handle,
.cp-timeline.is-scrubbing .cp-timeline-handle {
transform: translate(-50%, -50%) scale(1);
}
.cp-time-row {
display: flex;
align-items: center;
gap: 10px;
}
.cp-time {
color: #fff;
font-size: 12px;
font-variant-numeric: tabular-nums;
flex-shrink: 0;
min-width: 34px;
}
.cp-time-duration {
text-align: right;
}
.cp-transport {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
justify-content: center;
}
.cp-transport button {
position: relative;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: transparent;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.cp-play-btn {
width: 48px !important;
height: 48px !important;
background: rgba(255, 255, 255, 0.12) !important;
}
.cp-play-btn .icon-svg {
width: 22px;
height: 22px;
}
.cp-skip-back-btn .icon-svg,
.cp-skip-fwd-btn .icon-svg {
width: 20px;
height: 20px;
}
.cp-skip-amount {
position: absolute;
bottom: 2px;
font-size: 9px;
font-weight: 600;
background: rgba(0, 0, 0, 0.6);
border-radius: 6px;
padding: 0 3px;
pointer-events: none;
}
.cp-secondary-controls {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.cp-mute-btn,
.cp-pip-btn {
width: 34px;
height: 34px;
border-radius: 50%;
border: none;
background: transparent;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.cp-mute-btn .icon-svg,
.cp-pip-btn .icon-svg {
width: 18px;
height: 18px;
}
.cp-volume-range {
width: 64px;
accent-color: var(--accent);
}
.cp-format-btn {
height: 26px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.35);
background: rgba(0, 0, 0, 0.4);
color: #fff;
font-size: 11px;
font-weight: 600;
cursor: pointer;
}
.cp-format-menu {
position: absolute;
right: 16px;
bottom: 64px;
display: flex;
flex-direction: column;
background: rgba(20, 17, 13, 0.92);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
z-index: 6;
max-height: 40vh;
overflow-y: auto;
}
.cp-format-option {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 18px 9px 14px;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 13px;
text-align: left;
cursor: pointer;
white-space: nowrap;
}
/* A fixed-width tick column so every label starts on the same x, with only
the playing format's tick actually inked. */
.cp-format-option::before {
content: '\2713';
width: 1em;
flex: none;
opacity: 0;
font-size: 12px;
}
.cp-format-option:hover {
background: var(--bg-tertiary);
}
.cp-format-option.is-active {
color: var(--accent);
font-weight: 600;
}
.cp-format-option.is-active::before {
opacity: 1;
}
/* `.cp-error`/`.cp-replay-btn`/`.cp-format-menu` above set `display` on the
bare class so JS can toggle visibility via the `hidden` attribute alone;
these higher-specificity overrides keep that attribute effective (an
unconditional `display` on the class would otherwise beat the UA
`[hidden] { display: none }` rule). */
.cp-error[hidden],
.cp-replay-btn[hidden],
.cp-format-menu[hidden],
.cp-pip-btn[hidden],
.favorite-btn[hidden] {
display: none;
}
@media (max-width: 480px) {
.cp-title { font-size: 14px; }
.cp-volume-range { display: none; }
}
.error-toast {
position: fixed;
right: 20px;
bottom: 20px;
max-width: min(360px, 90vw);
background: rgba(0, 0, 0, 0.85);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
padding: 12px 14px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
z-index: 3000;
opacity: 0;
pointer-events: none;
transform: translateY(8px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.error-toast.show {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.error-toast button {
background: transparent;
border: none;
color: inherit;
cursor: pointer;
font-size: 16px;
}
body.theme-light .error-toast {
background: rgba(255, 255, 255, 0.95);
color: #000000;
border: 1px solid rgba(0, 0, 0, 0.12);
}
.update-banner {
position: fixed;
left: 50%;
bottom: 20px;
transform: translateX(-50%) translateY(8px);
max-width: min(420px, 92vw);
background: rgba(0, 0, 0, 0.88);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
padding: 12px 14px;
display: flex;
align-items: center;
gap: 14px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
z-index: 3100;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.update-banner.show {
opacity: 1;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
}
.update-banner button {
background: #ffffff;
color: #000000;
border: none;
border-radius: 6px;
padding: 6px 14px;
font: inherit;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
}
body.theme-light .update-banner {
background: rgba(255, 255, 255, 0.96);
color: #000000;
border: 1px solid rgba(0, 0, 0, 0.12);
}
body.theme-light .update-banner button {
background: #111111;
color: #ffffff;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-primary);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-secondary);
}
/* Mode toggle FAB (grid <-> reels) */
.mode-toggle-btn {
position: fixed;
right: 24px;
bottom: 90px;
width: 52px;
height: 52px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--bg-secondary);
backdrop-filter: blur(12px);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 10px 24px var(--shadow);
z-index: 2600;
transition: transform 0.2s ease, background 0.2s ease, opacity 0.4s ease;
}
.mode-toggle-btn:hover {
background: var(--bg-tertiary);
transform: scale(1.06);
}
.mode-toggle-btn:active {
transform: scale(0.94);
}
body.feed-mode-open .mode-toggle-btn {
background: rgba(0, 0, 0, 0.55);
border-color: rgba(255, 255, 255, 0.18);
}
body.feed-mode-open .mode-toggle-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
body.feed-mode-open .mode-toggle-btn .icon-svg {
filter: invert(100%) saturate(0%) !important;
}
/* Reels-style scrollable feed */
.feed-view {
display: none;
position: fixed;
inset: 0;
background: #000;
z-index: 2500;
}
.feed-view.open {
display: block;
}
.feed-scroll {
width: 100%;
height: 100%;
overflow-y: scroll;
scroll-snap-type: y mandatory;
scrollbar-width: none;
}
.feed-scroll::-webkit-scrollbar {
display: none;
}
.feed-sentinel {
height: 1px;
}
/* Stands in for the slides above the rendered window so scroll position and
snap points stay stable while slides are virtualized in and out. */
.feed-top-spacer {
width: 100%;
height: 0;
flex: none;
}
/* One screenful. It holds the pane tree rather than a video directly: with
several panes, a step shows that many videos at once and one swipe advances
the whole set. */
.feed-slide {
position: relative;
width: 100%;
height: 100vh;
height: 100dvh;
scroll-snap-align: start;
scroll-snap-stop: always;
overflow: hidden;
background: #000;
}
/* A branch of the tree: two children sharing the space, side by side or
stacked. Nesting these is what makes any arrangement reachable. */
.feed-split {
display: flex;
width: 100%;
height: 100%;
gap: 2px;
}
.feed-split.is-row { flex-direction: row; }
.feed-split.is-col { flex-direction: column; }
/* min-* is what stops a flex child refusing to shrink below its content. */
.feed-split > .feed-pane,
.feed-split > .feed-split {
flex: 1 1 0;
min-width: 0;
min-height: 0;
}
.feed-pane {
position: relative;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
background: #000;
}
.feed-pane-tools {
position: absolute;
top: 12px;
right: 12px;
display: flex;
gap: 6px;
/* Above the video, below the right-hand controls it sits beside. */
z-index: 5;
}
.feed-pane-btn {
width: 34px;
height: 34px;
border-radius: 50%;
border: 1px solid rgba(244, 232, 212, 0.18);
background: rgba(0, 0, 0, 0.45);
color: var(--text-primary);
font-size: 14px;
line-height: 1;
cursor: pointer;
backdrop-filter: blur(6px);
transition: background 0.2s ease, border-color 0.2s ease;
}
.feed-pane-btn:hover {
background: rgba(0, 0, 0, 0.7);
border-color: var(--border-hover);
}
.feed-pane-mute.is-muted {
border-color: var(--accent);
}
/* The HUD's idle fade takes the panel tools with it, like every other control. */
.feed-hud-idle .feed-pane-tools {
opacity: 0;
transition: opacity 0.4s ease;
}
.feed-poster,
.feed-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
.feed-poster {
transition: opacity 0.2s ease;
}
.feed-pane.is-loaded .feed-poster {
opacity: 0;
}
.feed-info {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 24px 20px 44px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), transparent);
color: #fff;
pointer-events: none;
transition: opacity 0.4s ease;
}
.feed-title {
font-size: 16px;
font-weight: 600;
margin: 0 0 4px;
white-space: nowrap;
overflow: hidden;
}
.feed-title-text {
display: inline-block;
padding-right: 28px;
transform: translateX(0);
}
.feed-title.has-marquee .feed-title-text {
animation: video-title-marquee var(--marquee-duration, 10s) linear infinite;
will-change: transform;
}
.feed-uploader {
font-size: 13px;
opacity: 0.8;
margin: 0;
}
.feed-timeline {
position: absolute;
left: 0;
right: 0;
bottom: 4px;
height: 28px;
display: flex;
align-items: center;
padding: 0 16px;
cursor: pointer;
touch-action: none;
z-index: 5;
}
.feed-timeline-track {
position: relative;
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.3);
border-radius: 999px;
overflow: visible;
transition: height 0.15s ease;
}
.feed-timeline:hover .feed-timeline-track,
.feed-timeline.is-scrubbing .feed-timeline-track {
height: 5px;
}
.feed-timeline-fill {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 0%;
background: #fff;
border-radius: 999px;
}
.feed-timeline-handle {
position: absolute;
top: 50%;
left: 0%;
width: 11px;
height: 11px;
margin-left: -5.5px;
background: #fff;
border-radius: 50%;
transform: translateY(-50%) scale(0);
transition: transform 0.15s ease;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
.feed-timeline:hover .feed-timeline-handle,
.feed-timeline.is-scrubbing .feed-timeline-handle {
transform: translateY(-50%) scale(1);
}
.feed-mute-btn {
position: fixed;
right: 24px;
bottom: 154px;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 2600;
transition: background 0.2s ease, opacity 0.4s ease;
}
.feed-mute-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.feed-mute-btn .icon-svg {
filter: invert(100%) saturate(0%) !important;
}
/* Per-slide favorite (heart) button on the feed HUD right rail. */
/* The right-hand controls stack upwards from just above the caption, rather
than sitting at fixed distances from the bottom. A pane can be a third of
the viewport tall, and viewport-scale offsets put these outside it -- clipped
by the pane's own overflow and unreachable. */
.feed-pane .feed-fav-btn,
.feed-pane .feed-pip-btn,
.feed-pane .feed-format-btn {
position: absolute;
right: 12px;
bottom: auto;
top: 56px;
}
.feed-pane .feed-pip-btn { top: 100px; }
.feed-pane .feed-format-btn { top: 144px; }
.feed-fav-btn {
position: absolute;
top: auto;
left: auto;
right: 24px;
bottom: 210px;
width: 44px;
height: 44px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.18);
background: rgba(0, 0, 0, 0.5);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
line-height: 1;
cursor: pointer;
z-index: 6;
transition: background 0.2s ease, transform 0.2s ease, opacity 0.4s ease;
}
.feed-fav-btn:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.05);
}
.feed-fav-btn.is-favorite {
color: #ff3b30;
border-color: rgba(255, 59, 48, 0.6);
background: rgba(255, 59, 48, 0.18);
}
/* Reels HUD right rail additions: PiP + quality, stacked above favorite. */
.feed-pip-btn {
position: absolute;
right: 24px;
bottom: 266px;
z-index: 6;
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.18);
transition: background 0.2s ease, opacity 0.4s ease;
}
.feed-pip-btn .icon-svg {
filter: invert(100%) saturate(0%);
}
.feed-format-btn {
position: absolute;
right: 24px;
bottom: 322px;
z-index: 6;
transition: opacity 0.4s ease;
}
.feed-format-menu {
right: 76px;
bottom: 322px;
}
.feed-flash {
z-index: 5;
}
/* Reels/TikTok mode: HUD auto-hides after a short idle. Elements stay
interactive (pointer-events untouched) so the buttons keep working while
invisible; any pointer/scroll activity reveals them again (see App.feed). */
.feed-hud-idle .feed-info,
.feed-hud-idle .feed-timeline,
.feed-hud-idle .feed-mute-btn,
.feed-hud-idle .feed-fav-btn,
.feed-hud-idle .feed-pip-btn,
.feed-hud-idle .feed-format-btn,
.feed-hud-idle .mode-toggle-btn {
opacity: 0;
}
/* =====================================================================
Modern enhancements (see enhance.js)
===================================================================== */
/* --- Card entrance: cards rise/fade in the first time they mount -------- */
@keyframes card-rise {
from { opacity: 0; transform: translateY(18px) scale(0.985); }
to { opacity: 1; transform: none; }
}
.video-card.is-revealing {
animation: card-rise 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both;
}
/* --- Cursor-tracking brass spotlight border on hover ------------------- */
.video-card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 1px;
background: radial-gradient(180px circle at var(--mx, 50%) var(--my, 50%),
rgba(201, 165, 103, 0.65), rgba(201, 165, 103, 0) 60%);
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
mask-composite: exclude;
opacity: 0;
transition: opacity 0.25s ease;
pointer-events: none;
z-index: 3;
}
.video-card:hover::after {
opacity: 1;
}
/* --- Thumbnail skeleton shimmer until the image paints ----------------- */
@keyframes thumb-shimmer {
from { background-position: 200% 0; }
to { background-position: -200% 0; }
}
.video-card img:not(.is-loaded) {
background-image: linear-gradient(100deg,
transparent 30%, rgba(255, 255, 255, 0.08) 50%, transparent 70%);
background-size: 200% 100%;
background-repeat: no-repeat;
animation: thumb-shimmer 1.3s ease-in-out infinite;
}
body.theme-light .video-card img:not(.is-loaded) {
background-image: linear-gradient(100deg,
transparent 30%, rgba(43, 33, 18, 0.06) 50%, transparent 70%);
}
/* --- Hover video preview (sits over the poster, under the buttons) ----- */
.card-preview {
position: absolute;
top: 0;
left: 0;
width: 100%;
object-fit: cover;
background: #000;
z-index: 0;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-card.is-previewing .card-preview {
opacity: 1;
}
/* --- Favorite heart: pop + expanding brass ring on add ----------------- */
@keyframes heart-pop {
0% { transform: scale(1); }
35% { transform: scale(1.35); }
100% { transform: scale(1); }
}
@keyframes heart-ring {
from { opacity: 0.8; transform: scale(0.6); }
to { opacity: 0; transform: scale(1.7); }
}
.favorite-btn.just-favorited {
animation: heart-pop 0.45s ease;
}
.favorite-btn.just-favorited::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
border: 2px solid var(--accent);
animation: heart-ring 0.5s ease forwards;
pointer-events: none;
}
/* --- Scroll progress bar ---------------------------------------------- */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 2px;
width: 0;
background: linear-gradient(90deg, var(--accent), var(--accent-hover));
z-index: 200;
transition: width 0.1s linear;
pointer-events: none;
}
/* --- Back-to-top FAB --------------------------------------------------- */
.back-to-top {
position: fixed;
right: 24px;
bottom: 154px;
width: 46px;
height: 46px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 20px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 10px 24px var(--shadow);
backdrop-filter: blur(12px);
opacity: 0;
transform: translateY(12px) scale(0.9);
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease, background 0.2s ease;
z-index: 2400;
}
.back-to-top.is-visible {
opacity: 1;
transform: none;
pointer-events: auto;
}
.back-to-top:hover {
background: var(--bg-tertiary);
border-color: var(--border-hover);
}
.back-to-top:active {
transform: scale(0.92);
}
/* --- Command palette (⌘K) --------------------------------------------- */
.command-palette {
position: fixed;
inset: 0;
z-index: 4000;
display: none;
align-items: flex-start;
justify-content: center;
padding-top: 14vh;
background: rgba(10, 8, 4, 0.55);
backdrop-filter: blur(6px);
}
.command-palette.open {
display: flex;
}
.cmdk-box {
width: min(560px, 92vw);
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: 0 30px 70px var(--shadow);
overflow: hidden;
animation: card-rise 0.2s ease both;
}
.cmdk-input {
width: 100%;
padding: 18px 20px;
border: none;
border-bottom: 1px solid var(--border);
background: transparent;
color: var(--text-primary);
font-size: 16px;
font-family: var(--font-body);
}
.cmdk-input::placeholder { color: var(--text-secondary); }
.cmdk-input:focus { outline: none; }
.cmdk-list {
max-height: 50vh;
overflow-y: auto;
padding: 8px;
}
.cmdk-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
text-align: left;
padding: 11px 14px;
border: none;
background: transparent;
color: var(--text-primary);
border-radius: 10px;
cursor: pointer;
font: inherit;
}
.cmdk-item.is-active {
background: var(--bg-tertiary);
}
.cmdk-label { font-size: 14px; }
.cmdk-hint {
font-size: 11px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text-secondary);
flex-shrink: 0;
}
/* --- Reels HUD polish: serif title + brass scrubber + mute pulse ------- */
.feed-title { font-family: var(--font-display); }
.feed-timeline-fill,
.feed-timeline-handle { background: #c9a567; }
@keyframes mute-pulse {
0% { transform: scale(0.8); opacity: 0.75; }
100% { transform: scale(1.5); opacity: 0; }
}
.feed-mute-btn.is-muted::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 50%;
border: 2px solid rgba(201, 165, 103, 0.7);
animation: mute-pulse 2s ease-out infinite;
pointer-events: none;
}