Files
jacuzzi/frontend/css/style.css
Simon a795442634 Choose a channel by what it is, not by its name in a list
A <select> of ninety channels tells the reader almost nothing: a wall of
bare names, nothing to say which site a name belongs to or what it
carries, and no way to look for one. The server has been sending far more
than the name all along -- a favicon, a description, tags, its own
groupings, and whether a channel still says "work in progress" -- so the
picker shows that.

It opens as a dialog in the site's own style: a search field, then the
server's groups as sections, each led by an "All <group>" row that browses
the whole group. Search matches the name, the id, the description, the
tags and the group, so "jav" finds Tokyo Motion (whose name never says it)
and "leaks" finds the OnlyFans mirrors. Arrows and Enter walk the list,
Escape closes it, and opening it with nothing typed scrolls to the channel
already being read. On a phone it fills the screen and leaves the field
unfocused -- the keyboard would cover the thing being chosen from.

Favicons load through attachThumbnail, so they get the same route race,
proxy fallback and retries as every other remote picture, with the
channel's initial behind them for the ones that never arrive.

The <select> was also where the command palette read its channel actions,
so the list lives in App.ui.channels now and the palette asks for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
2026-09-19 08:27:43 +00:00

2979 lines
64 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;
}
/* --- Channel picker ---------------------------------------------------- */
/* The trigger in the menu drawer: what's being read now, favicon and all. */
.channel-trigger {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 10px 12px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
font-family: var(--font-body);
font-size: 14px;
text-align: left;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
}
.channel-trigger:hover,
.channel-trigger:focus-visible {
border-color: var(--border-hover);
outline: none;
}
.channel-trigger-text {
flex: 1;
min-width: 0;
}
.channel-trigger-name,
.channel-trigger-note {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.channel-trigger-note {
font-size: 12px;
color: var(--text-secondary);
text-transform: none;
letter-spacing: 0;
}
.channel-trigger-caret {
flex-shrink: 0;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid var(--text-secondary);
}
/* A favicon, with the channel's initial behind it for the ones that never
arrive -- a row is never a name beside an empty square. */
.channel-mark {
position: relative;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 8px;
background: rgba(201, 165, 103, 0.14);
border: 1px solid var(--border);
overflow: hidden;
}
.channel-mark::before {
content: attr(data-letter);
font-family: var(--font-display);
font-size: 14px;
line-height: 1;
color: var(--accent);
}
.channel-favicon {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: contain;
padding: 3px;
background: inherit;
}
.channel-picker {
position: fixed;
inset: 0;
z-index: 4200;
display: none;
align-items: flex-start;
justify-content: center;
padding: 8vh 12px 12px 12px;
background: rgba(10, 8, 4, 0.55);
backdrop-filter: blur(6px);
}
.channel-picker.open { display: flex; }
.channel-picker-box {
display: flex;
flex-direction: column;
width: min(560px, 100%);
max-height: min(74vh, 700px);
/* Solid, not the usual translucent panel: the section headings stick over
the rows as they scroll, and a see-through heading is unreadable. */
background: var(--bg-primary);
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;
}
.channel-picker-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 10px 18px;
}
.channel-picker-head h3 {
font-family: var(--font-display);
font-size: 18px;
font-weight: 600;
}
.channel-search {
margin: 0 18px 12px 18px;
padding: 11px 14px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 10px;
color: var(--text-primary);
font-family: var(--font-body);
font-size: 14px;
}
.channel-search::placeholder { color: var(--text-secondary); }
.channel-search:focus { outline: none; border-color: var(--border-hover); }
.channel-picker-list {
flex: 1;
overflow-y: auto;
padding: 0 10px 10px 10px;
overscroll-behavior: contain;
}
.channel-section {
position: sticky;
top: 0;
z-index: 1;
padding: 10px 8px 6px 8px;
background: var(--bg-primary);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-secondary);
}
.channel-row {
display: flex;
align-items: flex-start;
gap: 12px;
width: 100%;
padding: 10px 12px;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
color: var(--text-primary);
font-family: var(--font-body);
text-align: left;
cursor: pointer;
}
.channel-row.is-active { background: var(--bg-tertiary); }
.channel-row.is-current {
border-color: var(--border-hover);
background: rgba(201, 165, 103, 0.10);
}
.channel-row-text {
flex: 1;
min-width: 0;
}
.channel-row-name {
display: block;
font-size: 14px;
}
.channel-row-note {
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin-top: 2px;
font-size: 12px;
line-height: 1.4;
color: var(--text-secondary);
}
.channel-row-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-top: 6px;
}
.channel-tag {
padding: 2px 7px;
border: 1px solid var(--border);
border-radius: 999px;
font-size: 10px;
letter-spacing: 0.04em;
color: var(--text-secondary);
}
.channel-row-flag {
flex-shrink: 0;
align-self: center;
font-size: 10px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--accent);
}
.channel-picker-empty {
padding: 8px 20px 20px 20px;
color: var(--text-secondary);
font-size: 13px;
}
@media (max-width: 720px) {
.channel-picker { padding: 0; }
.channel-picker-box {
width: 100%;
max-height: 100%;
height: 100%;
border: none;
border-radius: 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;
}