@import "tailwindcss";
/* ================================================================
§ 1 THEME BRIDGE — Tailwind ↔ CSS Custom Properties
================================================================
Maps semantic CSS variables (set per-theme below) into
Tailwind's @theme system so they work as utility classes.
e.g. bg-background, text-foreground, border-border, etc.
================================================================ */
@theme inline {
/* ── Surfaces ────────────────────────────────────────────── */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-surface: var(--surface);
--color-surface-hover: var(--surface-hover);
--color-surface-raised: var(--surface-raised);
/* ── Interactive ─────────────────────────────────────────── */
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
/* ── Status ────────────────────────────────────────────────── */
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
/* ── Borders & Focus ─────────────────────────────────────── */
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
/* ── Charts ──────────────────────────────────────────────── */
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
/* ── Sidebar ─────────────────────────────────────────────── */
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-section-divider: var(--sidebar-section-divider);
--color-sidebar-ring: var(--sidebar-ring);
/* ── Border Radius ───────────────────────────────────────── */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-pill: 9999px;
/* ── Shadows (dark-UI optimized) ─────────────────────────── */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.25);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4), 0 4px 6px -4px rgb(0 0 0 / 0.4);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.5), 0 8px 10px -6px rgb(0 0 0 / 0.5);
/* ── Animations ──────────────────────────────────────────── */
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-fade-in: fade-in 0.3s ease-out;
--animate-slide-up: slide-up 0.3s ease-out;
--animate-scale-in: scale-in 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
--animate-ken-burns-a: ken-burns-a 14s linear infinite;
--animate-ken-burns-b: ken-burns-b 18s linear infinite;
/* ── Motion: Duration ──────────────────────────────────────── */
--duration-instant: 0ms;
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 400ms;
--duration-glacial: 700ms;
--duration-cinematic: 1200ms;
--duration-drift: 2000ms;
/* Sidebar immersion collapse — see `.sidebar-surface`. */
--duration-sidebar-collapse: 300ms;
/* ── Motion: Easing ────────────────────────────────────────── */
--ease-default: cubic-bezier(0.4, 0, 0.2, 1);
/* Long, flat tail — keeps the moving edge from appearing to step near the end. */
--ease-sidebar-collapse: cubic-bezier(0.32, 0.72, 0, 1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-gentle: cubic-bezier(0.25, 0, 0.35, 1);
/* ── Font Family ───────────────────────────────────────────── */
--font-family-display: var(--font-display, var(--font-body));
/* ── Hero Backdrop Defaults ─────────────────────────────────── */
--hero-backdrop-brightness: 0.75;
--hero-backdrop-saturate: 0.9;
--hero-text-shadow: 0 18px 50px rgb(0 0 0 / 45%);
}
/* ================================================================
§ 2 KEYFRAMES
================================================================ */
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scale-in {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes pulse-opacity {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes ken-burns-a {
0% {
transform: scale(1) translate(0, 0);
}
50% {
transform: scale(1.06) translate(-0.5%, -0.3%);
}
100% {
transform: scale(1) translate(0, 0);
}
}
@keyframes ken-burns-b {
0% {
transform: scale(1.04) translate(0.3%, 0);
}
50% {
transform: scale(1) translate(-0.3%, 0.2%);
}
100% {
transform: scale(1.04) translate(0.3%, 0);
}
}
/* Soft radiating halo used behind the centered play button while paused —
communicates "tap to resume" without distracting during playback. */
@keyframes player-breathe {
0%,
100% {
box-shadow:
0 0 0 0 rgb(255 255 255 / 0.22),
0 20px 60px -18px rgb(0 0 0 / 0.7),
inset 0 1px 0 rgb(255 255 255 / 0.7);
}
60% {
box-shadow:
0 0 0 18px rgb(255 255 255 / 0),
0 20px 60px -18px rgb(0 0 0 / 0.7),
inset 0 1px 0 rgb(255 255 255 / 0.7);
}
}
/* Cinematic reveal for the floating control cluster — fades in slightly
below its resting position when controls become visible. Pairs with the
opacity transition applied via Tailwind utilities. */
@keyframes player-rise {
from {
opacity: 0;
transform: translateY(6px) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* ================================================================
VIEW TRANSITIONS — Page navigation animations
================================================================
Uses the View Transitions API for smooth cross-fades between
route changes. The browser captures old/new page snapshots
and animates between them.
================================================================ */
::view-transition-old(main-content) {
animation: 200ms cubic-bezier(0, 0, 0.2, 1) both fade-out;
}
::view-transition-new(main-content) {
animation: 300ms cubic-bezier(0, 0, 0.2, 1) both slide-up;
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(main-content),
::view-transition-new(main-content) {
animation: none;
}
:root {
--duration-fast: 0ms;
--duration-normal: 0ms;
--duration-cinematic: 0ms;
--duration-drift: 0ms;
--duration-glacial: 0ms;
--duration-slow: 150ms;
--duration-sidebar-collapse: 0ms;
--animate-ken-burns-a: none;
--animate-ken-burns-b: none;
}
.ambient-glow {
transition: none;
}
/* Collapsing the sidebar becomes an instant state change: the frame, the
content under it, and everything cross-fading inside all stop animating. */
.sidebar-surface,
.sidebar-inner,
.sidebar-fade,
.sidebar-row-shift,
.sidebar-main-stage,
.impersonation-banner {
transition: none;
}
.player-breathe,
.player-rise {
animation: none !important;
}
}
@media (forced-colors: active) {
* {
forced-color-adjust: auto;
}
}
/* ================================================================
§ 3 THEME DEFINITIONS
================================================================
Each theme sets ALL semantic tokens. Themes are activated via
the data-theme attribute on .
Token naming conventions:
--background Page background (darkest main surface)
--foreground Primary text color
--surface Default card/panel surface
--surface-hover Surface on hover
--surface-raised Elevated surface (modals, floating panels)
--card Card background (usually = surface)
--primary Primary accent/action color
--secondary Secondary element backgrounds
--muted Muted backgrounds
--accent Active/highlighted element backgrounds
--destructive Danger/error color
--border Default border color
--sidebar Sidebar background (often darkest surface)
================================================================ */
@layer base {
/* ── Global Status Tokens ──────────────────────────────────
Semantic status colors used across all themes. Individual
themes may override these if needed.
───────────────────────────────────────────────────────── */
:root {
--success: oklch(0.75 0.18 145);
--success-foreground: oklch(0.98 0.01 145);
--warning: oklch(0.8 0.15 85);
--warning-foreground: oklch(0.98 0.01 85);
--info: oklch(0.7 0.15 250);
--info-foreground: oklch(0.98 0.01 250);
}
/* ── Midnight Cinema ────────────────────────────────────── */
/* Inspired by premium streaming UIs: monochromatic, cinematic,
content-focused. White primary creates an elegant, theater-like
atmosphere where media artwork is the star. */
[data-theme="midnight-cinema"] {
--radius: 0.75rem;
--font-body: "Outfit", sans-serif;
--font-display: "Outfit", sans-serif;
/* Surfaces: near-black with subtle cool undertone */
--background: #141417;
--foreground: #e8e8ec;
--card: #1c1c20;
--card-foreground: #e8e8ec;
--popover: #18181c;
--popover-foreground: #e8e8ec;
/* Primary: white — for play buttons, key CTAs, active indicators */
--primary: #e8e8ec;
--primary-foreground: #141417;
/* Secondary & Muted: subtle surface variations */
--secondary: #232328;
--secondary-foreground: #d0d0d6;
--muted: #1c1c20;
/* Lightened from #6e6e78 (~3.4:1) to meet WCAG AA: ~6.3:1 on --background,
~5.3:1 even on the lightest chip surface (#232328). */
--muted-foreground: #9696a0;
--accent: #232328;
--accent-foreground: #e8e8ec;
/* Destructive: warm red for danger actions */
--destructive: #ef4444;
--destructive-foreground: #ffffff;
/* Borders: barely visible, just enough structure */
--border: #28282e;
--input: #1c1c20;
--ring: #e8e8ec;
/* Charts: desaturated pastels that fit the cinema aesthetic */
--chart-1: #8b9cf7;
--chart-2: #7ec8e3;
--chart-3: #81c995;
--chart-4: #e8a87c;
--chart-5: #c78dbd;
/* Sidebar: darkest surface, creating depth */
--sidebar: #0f0f12;
--sidebar-foreground: #e8e8ec;
--sidebar-primary: #e8e8ec;
--sidebar-primary-foreground: #0f0f12;
--sidebar-accent: #1c1c20;
--sidebar-accent-foreground: #e8e8ec;
--sidebar-border: #232328;
--sidebar-section-divider: #2e2e35;
--sidebar-ring: #e8e8ec;
/* Surfaces: layered depth */
--surface: #1c1c20;
--surface-hover: #232328;
--surface-raised: #28282e;
--ambient: #e8e8ec;
--ambient-glow-opacity: 0.08;
}
[data-theme="cinema-light"] {
--radius: 0.75rem;
--font-body: "Outfit", sans-serif;
--font-display: "Outfit", sans-serif;
/* Surfaces: warm off-white with cool undertone */
--background: #f4f4f6;
--foreground: #1a1a1e;
--card: #ffffff;
--card-foreground: #1a1a1e;
--popover: #ffffff;
--popover-foreground: #1a1a1e;
/* Primary: near-black — inverted from Cinema Dark */
--primary: #1a1a1e;
--primary-foreground: #f4f4f6;
/* Secondary & Muted: subtle surface variations */
--secondary: #e8e8ec;
--secondary-foreground: #3a3a42;
--muted: #ebebef;
/* Darkened from #78787f (~4.0:1) to meet WCAG AA on light surfaces
(~4.9:1 on the #e8e8ec chip, ~5.4:1 on --background). */
--muted-foreground: #63636b;
--accent: #e8e8ec;
--accent-foreground: #1a1a1e;
/* Destructive: warm red for danger actions */
--destructive: #dc2626;
--destructive-foreground: #ffffff;
/* Borders: soft gray, enough structure without weight */
--border: #d8d8de;
--input: #e8e8ec;
--ring: #1a1a1e;
/* Charts: richer tones for light backgrounds */
--chart-1: #6366f1;
--chart-2: #0ea5e9;
--chart-3: #22c55e;
--chart-4: #f59e0b;
--chart-5: #a855f7;
/* Sidebar: slightly darker than background for depth */
--sidebar: #eaeaee;
--sidebar-foreground: #1a1a1e;
--sidebar-primary: #1a1a1e;
--sidebar-primary-foreground: #f4f4f6;
--sidebar-accent: #dfdfe5;
--sidebar-accent-foreground: #1a1a1e;
--sidebar-border: #d0d0d8;
--sidebar-section-divider: #c2c2cc;
--sidebar-ring: #1a1a1e;
/* Surfaces: layered depth */
--surface: #ebebef;
--surface-hover: #e0e0e6;
--surface-raised: #ffffff;
--ambient: #1a1a1e;
--ambient-glow-opacity: 0.05;
/* Hero: light-mode overrides */
--hero-backdrop-brightness: 1.15;
--hero-backdrop-saturate: 0.6;
--hero-text-shadow: 0 1px 3px rgb(255 255 255 / 60%);
}
[data-theme="cobalt-studio"] {
--radius: 0.75rem;
--font-body: "Outfit", sans-serif;
--font-display: "Outfit", sans-serif;
--background: #101722;
--foreground: #f4f8ff;
--card: #151e2b;
--card-foreground: #f4f8ff;
--popover: #121a25;
--popover-foreground: #f4f8ff;
--primary: #78aefc;
--primary-foreground: #0f1722;
--secondary: #1b2634;
--secondary-foreground: #d7e2f1;
--muted: #151e2b;
--muted-foreground: #90a0b5;
--accent: #203043;
--accent-foreground: #f4f8ff;
--destructive: #ef6b73;
--destructive-foreground: #ffffff;
--border: #28384d;
--input: #182231;
--ring: #78aefc;
--chart-1: #78aefc;
--chart-2: #87d2ff;
--chart-3: #6ec7ba;
--chart-4: #b9c7ff;
--chart-5: #f18d8d;
--sidebar: #0c131d;
--sidebar-foreground: #f4f8ff;
--sidebar-primary: #78aefc;
--sidebar-primary-foreground: #0f1722;
--sidebar-accent: #172231;
--sidebar-accent-foreground: #f4f8ff;
--sidebar-border: #1f2a39;
--sidebar-section-divider: #283a4e;
--sidebar-ring: #78aefc;
--surface: #151e2b;
--surface-hover: #1d2a3b;
--surface-raised: #223245;
--ambient: #78aefc;
--ambient-glow-opacity: 0.11;
}
[data-theme="oxblood-noir"] {
--radius: 0.75rem;
--font-body: "Outfit", sans-serif;
--font-display: "Outfit", sans-serif;
--background: #171113;
--foreground: #f8f2f3;
--card: #20181b;
--card-foreground: #f8f2f3;
--popover: #1b1417;
--popover-foreground: #f8f2f3;
--primary: #d16a78;
--primary-foreground: #180f12;
--secondary: #281d21;
--secondary-foreground: #decfd2;
--muted: #20181b;
--muted-foreground: #a28e95;
--accent: #322228;
--accent-foreground: #f8f2f3;
--destructive: #f08080;
--destructive-foreground: #ffffff;
--border: #3b2830;
--input: #251a1f;
--ring: #d16a78;
--chart-1: #d16a78;
--chart-2: #f08e7a;
--chart-3: #c8a0b8;
--chart-4: #8aa0c6;
--chart-5: #e1b86e;
--sidebar: #120d0f;
--sidebar-foreground: #f8f2f3;
--sidebar-primary: #d16a78;
--sidebar-primary-foreground: #180f12;
--sidebar-accent: #21161a;
--sidebar-accent-foreground: #f8f2f3;
--sidebar-border: #2c1d23;
--sidebar-section-divider: #3a252e;
--sidebar-ring: #d16a78;
--surface: #20181b;
--surface-hover: #2a2025;
--surface-raised: #34262c;
--ambient: #d16a78;
--ambient-glow-opacity: 0.11;
}
[data-theme="evergreen-studio"] {
--radius: 0.75rem;
--font-body: "Outfit", sans-serif;
--font-display: "Outfit", sans-serif;
--background: #101715;
--foreground: #f2f8f5;
--card: #16201d;
--card-foreground: #f2f8f5;
--popover: #131b19;
--popover-foreground: #f2f8f5;
--primary: #5bc39d;
--primary-foreground: #0d1513;
--secondary: #1b2824;
--secondary-foreground: #d1e0d9;
--muted: #16201d;
--muted-foreground: #91a39c;
--accent: #20322d;
--accent-foreground: #f2f8f5;
--destructive: #f07a7a;
--destructive-foreground: #ffffff;
--border: #284038;
--input: #182420;
--ring: #5bc39d;
--chart-1: #5bc39d;
--chart-2: #7dd8bd;
--chart-3: #7eb7a4;
--chart-4: #88a9cf;
--chart-5: #e0b66b;
--sidebar: #0c1210;
--sidebar-foreground: #f2f8f5;
--sidebar-primary: #5bc39d;
--sidebar-primary-foreground: #0d1513;
--sidebar-accent: #16221e;
--sidebar-accent-foreground: #f2f8f5;
--sidebar-border: #1d2f2a;
--sidebar-section-divider: #253d35;
--sidebar-ring: #5bc39d;
--surface: #16201d;
--surface-hover: #1e2b27;
--surface-raised: #263732;
--ambient: #5bc39d;
--ambient-glow-opacity: 0.1;
}
}
/* ================================================================
§ 4 BASE STYLES
================================================================ */
@layer base {
* {
border-color: var(--border);
}
html {
font-size: 16px;
font-weight: 400;
--ui-text-scale-factor: 1;
}
html[data-text-scale="large"] {
font-size: 18px;
--ui-text-scale-factor: 1.12;
}
html[data-text-scale="x-large"] {
font-size: 20px;
--ui-text-scale-factor: 1.24;
}
html[data-text-weight="strong"] {
--font-weight-ui: 520;
--font-weight-medium-ui: 600;
--font-weight-semibold-ui: 700;
--font-weight-bold-ui: 800;
}
html[data-text-weight="default"] {
--font-weight-ui: 400;
--font-weight-medium-ui: 500;
--font-weight-semibold-ui: 600;
--font-weight-bold-ui: 700;
}
html[data-high-contrast="true"] {
--foreground: #ffffff;
--ring: oklch(0.95 0 0);
--muted-foreground: color-mix(in srgb, white 72%, var(--muted-foreground));
--border: color-mix(in srgb, white 34%, var(--border));
--input: color-mix(in srgb, white 16%, var(--input));
--surface: color-mix(in srgb, white 10%, var(--surface));
--surface-hover: color-mix(in srgb, white 16%, var(--surface-hover));
--surface-raised: color-mix(in srgb, white 20%, var(--surface-raised));
--accent: color-mix(in srgb, white 14%, var(--accent));
--ambient-glow-opacity: 0.04;
}
html[data-high-contrast="true"] .surface-panel-subtle,
html[data-high-contrast="true"] .surface-panel,
html[data-high-contrast="true"] .glass,
html[data-high-contrast="true"] .glass-subtle {
border-color: color-mix(in srgb, var(--border) 88%, white 12%);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.08),
0 12px 36px -24px rgb(0 0 0 / 0.75);
}
html[data-high-contrast="true"] .text-muted-foreground {
color: var(--muted-foreground) !important;
}
html[data-high-contrast="true"] .border-border,
html[data-high-contrast="true"] .border-border\/50,
html[data-high-contrast="true"] .border-border\/60,
html[data-high-contrast="true"] .border-border\/70,
html[data-high-contrast="true"] .border-border\/80 {
border-color: var(--border) !important;
}
html[data-text-weight="strong"] .font-medium {
font-weight: var(--font-weight-medium-ui, 600) !important;
}
html[data-text-weight="strong"] .font-semibold {
font-weight: var(--font-weight-semibold-ui, 700) !important;
}
html[data-text-weight="strong"] .font-bold {
font-weight: var(--font-weight-bold-ui, 800) !important;
}
html[data-text-scale="large"] .text-\[8px\],
html[data-text-scale="x-large"] .text-\[8px\] {
font-size: calc(8px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[9px\],
html[data-text-scale="x-large"] .text-\[9px\] {
font-size: calc(9px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[10px\],
html[data-text-scale="x-large"] .text-\[10px\] {
font-size: calc(10px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[11px\],
html[data-text-scale="x-large"] .text-\[11px\] {
font-size: calc(11px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[12px\],
html[data-text-scale="x-large"] .text-\[12px\] {
font-size: calc(12px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[13px\],
html[data-text-scale="x-large"] .text-\[13px\] {
font-size: calc(13px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[14px\],
html[data-text-scale="x-large"] .text-\[14px\] {
font-size: calc(14px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[15px\],
html[data-text-scale="x-large"] .text-\[15px\] {
font-size: calc(15px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[17px\],
html[data-text-scale="x-large"] .text-\[17px\] {
font-size: calc(17px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[20px\],
html[data-text-scale="x-large"] .text-\[20px\] {
font-size: calc(20px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[28px\],
html[data-text-scale="x-large"] .text-\[28px\] {
font-size: calc(28px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[32px\],
html[data-text-scale="x-large"] .text-\[32px\] {
font-size: calc(32px * var(--ui-text-scale-factor)) !important;
}
html[data-text-scale="large"] .text-\[38px\],
html[data-text-scale="x-large"] .text-\[38px\] {
font-size: calc(38px * var(--ui-text-scale-factor)) !important;
}
body {
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-body);
font-weight: var(--font-weight-ui, 400);
background-image:
radial-gradient(
circle at top,
color-mix(in srgb, var(--ambient) 10%, transparent),
transparent 32%
),
linear-gradient(
to bottom,
color-mix(in srgb, var(--background) 92%, transparent),
var(--background)
);
min-height: 100dvh;
}
}
/* ================================================================
§ 5 COMPONENT UTILITY CLASSES
================================================================
Reusable patterns extracted from the design system.
See ../docs/design-system.md for pattern documentation.
================================================================ */
@layer components {
/* ── Hero Gradient ───────────────────────────────────────── */
/* Place over a hero/backdrop image to create a cinematic
bottom-to-top gradient fade into the page background.
Usage:
// optional vignette
*/
.hero-gradient {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
var(--background) 0%,
color-mix(in srgb, var(--background) 92%, transparent) 20%,
color-mix(in srgb, var(--background) 55%, transparent) 50%,
color-mix(in srgb, var(--background) 20%, transparent) 75%,
transparent 100%
);
pointer-events: none;
}
.hero-gradient-radial {
position: absolute;
inset: 0;
background: radial-gradient(
ellipse at center,
transparent 0%,
color-mix(in srgb, var(--background) 30%, transparent) 70%,
var(--background) 100%
);
pointer-events: none;
}
/* Side gradient — fades from left edge for sidebar overlap */
.hero-gradient-left {
position: absolute;
inset: 0;
background: linear-gradient(
to right,
var(--background) 0%,
color-mix(in srgb, var(--background) 80%, transparent) 20%,
color-mix(in srgb, var(--background) 40%, transparent) 40%,
transparent 60%
);
pointer-events: none;
}
.hero-vignette {
position: absolute;
inset: 0;
background:
linear-gradient(
90deg,
color-mix(in srgb, var(--background) 92%, transparent) 0%,
color-mix(in srgb, var(--background) 64%, transparent) 28%,
transparent 58%
),
linear-gradient(
to top,
var(--background) 0%,
color-mix(in srgb, var(--background) 75%, transparent) 28%,
transparent 62%
);
pointer-events: none;
}
/* Stronger bottom fade for edge-to-edge marquee heroes. The hero
should melt into the page background rather than stop against a
border — background reaches full opacity by ~35% up the frame. */
.hero-gradient-strong {
position: absolute;
inset: 0;
background: linear-gradient(
to top,
var(--background) 0%,
var(--background) 12%,
color-mix(in srgb, var(--background) 88%, transparent) 30%,
color-mix(in srgb, var(--background) 55%, transparent) 55%,
color-mix(in srgb, var(--background) 18%, transparent) 80%,
transparent 100%
);
pointer-events: none;
}
/* Top scrim for the overlay marquee header — keeps eyebrow / tabs
legible over bright backdrops without a hard bar. */
.hero-top-scrim {
position: absolute;
inset: 0 0 auto 0;
height: 180px;
background: linear-gradient(
to bottom,
color-mix(in srgb, var(--background) 70%, transparent) 0%,
color-mix(in srgb, var(--background) 30%, transparent) 55%,
transparent 100%
);
pointer-events: none;
}
/* ── Ambient Glow ──────────────────────────────────────────── */
/* Radial gradient overlay tinted by --ambient color. Applied to
hero sections for atmospheric color bleed from featured content.
Usage:
(sibling to hero-gradient) */
.ambient-glow {
position: absolute;
inset: 0;
background: radial-gradient(
ellipse at 50% 80%,
var(--ambient, var(--primary)) 0%,
transparent 70%
);
opacity: var(--ambient-glow-opacity, 0.12);
pointer-events: none;
transition:
background var(--duration-cinematic) var(--ease-gentle),
opacity var(--duration-cinematic) var(--ease-gentle);
}
/* ── Sidebar Immersion ─────────────────────────────────────── */
/* The desktop sidebar is physically 260px wide in EVERY state. Entering a
detail page snaps the real layout — main's margin, the impersonation
banner, `--app-sidebar-offset` — from 260px to 64px in one pass, and the
sidebar and main content are held at their old visual positions for one
frame, then travel together to the 64px rail. The item route commits its
lightweight shell first; metadata and artwork stay gated until this
compositor motion has completed.
Nothing here touches width, margin, or any other layout property, so the
document is laid out once per navigation instead of once per frame. Holding
the surface at a constant size also means its 40px backdrop blur is
rasterized against unchanging geometry rather than re-blurred each frame. */
/* Two transforms that cancel out. `.sidebar-surface` is a 260px frame with
`overflow: hidden` that slides 196px left, so its right edge — and the
border drawn on it — sweeps from x=260 down to x=64. `.sidebar-inner`
slides the same distance the other way, so its content stays pinned to the
viewport and is progressively cut off by the frame's edge.
Why not animate the clip directly: Chrome runs `clip-path` transitions on
the main thread. Entering a detail page blocks that thread for longer than
the animation lasts (267ms and 209ms frozen frames measured on a real
navigation), which swallowed the collapse whole. `transform` is composited
and keeps advancing through exactly the same block. */
.sidebar-surface {
overflow: hidden;
transform: translateX(0);
transition: transform var(--duration-sidebar-collapse) var(--ease-sidebar-collapse);
}
.sidebar-surface[data-collapsed="true"] {
transform: translateX(-196px);
}
.sidebar-inner {
transform: translateX(0);
transition: transform var(--duration-sidebar-collapse) var(--ease-sidebar-collapse);
}
.sidebar-surface[data-collapsed="true"] .sidebar-inner {
transform: translateX(196px);
}
/* Main's margin snaps directly to the destination so the document lays out
once. Until the route is ready to animate, an inverse transform places the
new layout at the old visual offset. Removing that compensation on the
same state change that moves the sidebar keeps their edges locked together. */
@media (min-width: 64rem) {
.sidebar-main-stage {
transform: none;
transition: transform var(--duration-sidebar-collapse) var(--ease-sidebar-collapse);
}
.sidebar-main-stage[data-sidebar-target-collapsed="true"]:not(
[data-sidebar-visual-collapsed="true"]
) {
transform: translateX(196px);
transition: none;
}
.sidebar-main-stage:not(
[data-sidebar-target-collapsed="true"]
)[data-sidebar-visual-collapsed="true"] {
transform: translateX(-196px);
transition: none;
}
}
/* Sidebar contents keep their expanded layout in both states — only opacity
and transform change, so the nav never reflows mid-animation. Labels that
used to animate `max-width` now just fade; the frame does the hiding. */
.sidebar-fade {
transition: opacity var(--duration-sidebar-collapse) var(--ease-sidebar-collapse);
}
.sidebar-row-shift {
transition: transform var(--duration-sidebar-collapse) var(--ease-sidebar-collapse);
}
.sidebar-surface[data-collapsed="true"] .sidebar-row-shift {
/* Slides the library rows left by their reserved chevron slot so their
icons line up with the rest of the rail's icon column. */
transform: translateX(var(--sidebar-row-shift, 0px));
}
/* ── Sidebar Scrollbar ────────────────────────────────────── */
/* Thin, auto-hiding scrollbar that blends with the sidebar. */
.sidebar-scroll {
scrollbar-width: thin;
scrollbar-color: transparent transparent;
transition: scrollbar-color 0.2s ease;
}
.sidebar-scroll:hover {
scrollbar-color: color-mix(in srgb, var(--sidebar-foreground) 20%, transparent) transparent;
}
/* WebKit (Chrome, Safari, Edge) */
.sidebar-scroll::-webkit-scrollbar {
width: 6px;
}
.sidebar-scroll::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-scroll::-webkit-scrollbar-thumb {
background: transparent;
border-radius: 3px;
transition: background 0.2s ease;
}
.sidebar-scroll:hover::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--sidebar-foreground) 20%, transparent);
}
.sidebar-scroll:hover::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--sidebar-foreground) 35%, transparent);
}
/* ── Overlay Scrollbar ─────────────────────────────────── */
/* Thin, subtle scrollbar for dialogs and overlay panels. */
.overlay-scroll {
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--foreground) 15%, transparent) transparent;
}
.overlay-scroll::-webkit-scrollbar {
width: 6px;
}
.overlay-scroll::-webkit-scrollbar-track {
background: transparent;
}
.overlay-scroll::-webkit-scrollbar-thumb {
background: color-mix(in srgb, var(--foreground) 15%, transparent);
border-radius: 3px;
}
.overlay-scroll::-webkit-scrollbar-thumb:hover {
background: color-mix(in srgb, var(--foreground) 30%, transparent);
}
/* ── Glass Surfaces ──────────────────────────────────────── */
/* Frosted glass effect for floating controls, overlay panels.
Usage: ...
*/
.glass {
background: color-mix(in srgb, var(--surface) 70%, transparent);
backdrop-filter: blur(20px) saturate(1.2);
-webkit-backdrop-filter: blur(20px) saturate(1.2);
border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.06),
0 20px 50px -28px rgb(0 0 0 / 0.65);
}
.glass-subtle {
background: color-mix(in srgb, var(--surface) 40%, transparent);
backdrop-filter: blur(12px) saturate(1.1);
-webkit-backdrop-filter: blur(12px) saturate(1.1);
border: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.05);
}
/* Less-transparent glass for small chips that sit over busy cover art
(manga count/status pills) so the label stays legible. */
.glass-chip {
background: color-mix(in srgb, var(--surface) 78%, transparent);
backdrop-filter: blur(12px) saturate(1.1);
-webkit-backdrop-filter: blur(12px) saturate(1.1);
border: 1px solid color-mix(in srgb, var(--border) 45%, transparent);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06);
}
/* ── Terminal Surface ──────────────────────────────────────── */
/* Scoped terminal aesthetic for ffmpeg console and similar.
Usage: ...
*/
.terminal-surface {
--terminal-bg: oklch(0.15 0.01 260);
--terminal-fg: oklch(0.85 0.15 155);
--terminal-muted: oklch(0.65 0.1 155);
--terminal-border: oklch(0.3 0.05 155);
}
.glass-dark {
background: color-mix(in srgb, var(--background) 80%, transparent);
backdrop-filter: blur(24px) saturate(1.3);
-webkit-backdrop-filter: blur(24px) saturate(1.3);
border: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.04),
0 24px 60px -30px rgb(0 0 0 / 0.72);
}
/* ── Video Player — Cinema Glass ─────────────────────────────
Bespoke surfaces for the floating player HUD. Lives on a
black void (always on top of a video) so tokens here are
hard-coded monochrome — independent of the app's theme. */
/* Primary play/pause disc — sits at the center of the frame. */
.player-disc-primary {
background: linear-gradient(180deg, #ffffff 0%, #f3f3f5 100%);
color: #0a0a0b;
box-shadow:
0 20px 60px -18px rgb(0 0 0 / 0.7),
inset 0 1px 0 rgb(255 255 255 / 0.7);
transition:
transform var(--duration-fast) var(--ease-spring),
box-shadow var(--duration-normal) var(--ease-default),
background var(--duration-fast) var(--ease-default);
}
.player-disc-primary:hover {
transform: scale(1.04);
}
.player-disc-primary:active {
transform: scale(0.97);
}
/* Secondary floating control (skip ±, prev/next episode). A
barely-there glass disc that gains opacity on hover so the
frame stays clear during playback. */
.player-disc-secondary {
background: rgb(255 255 255 / 0.06);
color: rgb(255 255 255 / 0.92);
border: 1px solid rgb(255 255 255 / 0.1);
backdrop-filter: blur(14px) saturate(1.1);
-webkit-backdrop-filter: blur(14px) saturate(1.1);
box-shadow:
0 10px 30px -14px rgb(0 0 0 / 0.6),
inset 0 1px 0 rgb(255 255 255 / 0.08);
transition:
background var(--duration-fast) var(--ease-default),
transform var(--duration-fast) var(--ease-spring),
border-color var(--duration-fast) var(--ease-default);
}
.player-disc-secondary:hover {
background: rgb(255 255 255 / 0.14);
border-color: rgb(255 255 255 / 0.22);
transform: scale(1.06);
}
.player-disc-secondary:active {
transform: scale(0.95);
}
/* Breathing halo behind the primary disc when paused. */
.player-disc-primary[data-paused="true"] {
animation: player-breathe 2.6s ease-in-out infinite;
}
/* Bottom HUD rail — title/time on the left, utilities on the right. */
.player-hud {
background: linear-gradient(
to top,
rgb(0 0 0 / 0.82) 0%,
rgb(0 0 0 / 0.56) 55%,
rgb(0 0 0 / 0) 100%
);
}
/* Full-frame vignette for control legibility without washing out
the picture. Two stacked gradients: a soft top scrim for the
exit/title chrome and a stronger bottom scrim for the HUD. */
.player-scrim {
background:
linear-gradient(to bottom, rgb(0 0 0 / 0.55) 0%, rgb(0 0 0 / 0.18) 14%, rgb(0 0 0 / 0) 28%),
radial-gradient(ellipse 70% 50% at 50% 50%, rgb(0 0 0 / 0.18), transparent 70%);
}
/* Floating cluster entrance animation — fades in with a gentle rise
when the controls reveal themselves. */
.player-rise {
animation: player-rise var(--duration-normal) var(--ease-gentle);
}
/* Subtle utility-icon button used in the bottom HUD rail. Relies on
opacity to recede during playback; a thin active underline signals
which secondary control is currently on. */
.player-utility-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
color: rgb(255 255 255 / 0.78);
background: transparent;
border: none;
cursor: pointer;
transition:
color var(--duration-fast) var(--ease-default),
background-color var(--duration-fast) var(--ease-default);
}
.player-utility-btn:hover {
color: #fff;
background: rgb(255 255 255 / 0.08);
}
.player-utility-btn:focus-visible {
outline: 2px solid rgb(255 255 255 / 0.7);
outline-offset: 2px;
}
.player-utility-btn[data-active="true"] {
color: #fff;
}
.player-utility-btn[data-active="true"]::after {
content: "";
position: absolute;
bottom: 0.22rem;
left: 50%;
width: 0.28rem;
height: 0.28rem;
border-radius: 9999px;
background: #e8b86e; /* warm projector-lamp amber */
transform: translateX(-50%);
box-shadow: 0 0 8px rgb(232 184 110 / 0.7);
}
/* Vertical divider between title cluster and utility rail. */
.player-hud-divider {
width: 1px;
align-self: stretch;
background: linear-gradient(
to bottom,
transparent,
rgb(255 255 255 / 0.14) 20%,
rgb(255 255 255 / 0.14) 80%,
transparent
);
margin-inline: 0.25rem;
}
/* Range-input thumb for the subtitle appearance panel's opacity slider.
Mirrors the seek bar's hover thumb so form controls feel native to the
player chrome rather than browser-default. */
.player-subtitle-opacity-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
border-radius: 9999px;
background: #ffffff;
border: none;
box-shadow:
0 2px 8px rgb(0 0 0 / 0.5),
inset 0 0 0 1px rgb(0 0 0 / 0.12);
cursor: pointer;
}
.player-subtitle-opacity-slider::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 9999px;
background: #ffffff;
border: none;
box-shadow:
0 2px 8px rgb(0 0 0 / 0.5),
inset 0 0 0 1px rgb(0 0 0 / 0.12);
cursor: pointer;
}
/* ── Media Card ──────────────────────────────────────────── */
/* Hover effect for poster/backdrop cards.
Scale is applied to the image inside the overflow-hidden
.media-card-image container so it stays visually contained
and doesn't overlap adjacent carousels.
Usage: */
.media-card {
transition:
transform var(--duration-normal) var(--ease-gentle),
filter var(--duration-fast) var(--ease-default);
}
.media-card:hover {
filter: brightness(1.1);
transform: translateY(-4px);
}
/* Card image container with consistent rounded corners */
.media-card-image {
overflow: hidden;
border-radius: var(--radius-xl);
background-color: var(--surface);
border: 1px solid color-mix(in srgb, var(--border) 65%, transparent);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.05),
0 24px 40px -26px rgb(0 0 0 / 0.8);
}
.media-card-image img {
transition:
transform var(--duration-normal) var(--ease-gentle),
opacity var(--duration-normal) var(--ease-default);
}
.media-card:hover .media-card-image img {
transform: scale(1.06);
}
/* ── Episode Row ─────────────────────────────────────────── */
/* Horizontal episode list item with hover highlight.
Usage:
E1 Title ...
*/
.episode-row {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
transition: background-color var(--duration-fast) var(--ease-default);
cursor: pointer;
}
.episode-row:hover {
background-color: var(--surface-hover);
}
.episode-row:last-child {
border-bottom: none;
}
/* ── Metadata Badge ──────────────────────────────────────── */
/* Small inline badge for metadata (year, rating, duration).
Usage: PG-13 */
.metadata-badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.6rem;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 600;
line-height: 1rem;
letter-spacing: 0.04em;
text-transform: uppercase;
background-color: color-mix(in srgb, var(--foreground) 8%, transparent);
color: color-mix(in srgb, var(--foreground) 72%, var(--muted-foreground));
border: 1px solid color-mix(in srgb, var(--border) 55%, transparent);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
/* ── Pill Button ─────────────────────────────────────────── */
/* Fully-rounded button shape for primary actions.
Usage: ▶ Play */
.pill {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.78rem 1.4rem;
border-radius: 9999px;
font-size: 0.84rem;
font-weight: 700;
letter-spacing: 0.01em;
transition: all var(--duration-fast) var(--ease-default);
}
.pill-primary {
background-color: var(--primary);
color: var(--primary-foreground);
box-shadow: 0 16px 36px -20px color-mix(in srgb, var(--primary) 60%, transparent);
}
.pill-primary:hover {
filter: brightness(0.96);
transform: translateY(-1px);
}
.pill-secondary {
background-color: var(--secondary);
color: var(--secondary-foreground);
}
.pill-secondary:hover {
background-color: var(--surface-hover);
}
.pill-glass {
background: color-mix(in srgb, var(--foreground) 12%, transparent);
color: var(--foreground);
border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.pill-glass:hover {
background: color-mix(in srgb, var(--foreground) 18%, transparent);
}
/* ── Action Circle ───────────────────────────────────────── */
/* Circular icon button for secondary actions (shuffle, download, etc).
Usage: */
.action-circle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
background-color: color-mix(in srgb, var(--foreground) 10%, transparent);
color: var(--foreground);
transition: background-color var(--duration-fast) var(--ease-default);
cursor: pointer;
border: none;
}
.action-circle:hover {
background-color: color-mix(in srgb, var(--foreground) 18%, transparent);
}
/* ── Tab Bar ─────────────────────────────────────────────── */
/* Pill-style tab navigation (Recommended | Browse | etc).
Usage:
Browse
Collections
*/
.tab-bar {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
border-radius: var(--radius-lg);
}
.tab-item {
padding: 0.375rem 1rem;
border-radius: var(--radius-md);
font-size: 0.875rem;
font-weight: 500;
color: var(--muted-foreground);
transition: all var(--duration-fast) var(--ease-default);
cursor: pointer;
background: transparent;
border: none;
}
.tab-item:hover {
color: var(--foreground);
background-color: var(--surface-hover);
}
.tab-active {
color: var(--foreground);
background-color: var(--surface);
border: 1px solid var(--border);
}
/* ── Carousel Dots ───────────────────────────────────────── */
/* Indicator dots for hero carousels.
Usage:
*/
.carousel-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
}
.dot {
width: 0.42rem;
height: 0.42rem;
border-radius: 9999px;
background-color: color-mix(in srgb, var(--foreground) 26%, transparent);
border: none;
padding: 0;
cursor: pointer;
transition: all var(--duration-normal) var(--ease-default);
}
.dot-active {
width: 1.5rem;
background-color: var(--primary);
}
/* ── Library Marquee Header ───────────────────────────────── */
/* Shared sticky header for the Library page. Sits transparently
over the hero on the Recommended tab, and switches to a glass
surface on scroll or when no hero backdrop is behind it. */
.library-marquee-header {
/* Mobile: non-sticky so the tabs scroll with the hero instead of hovering
over it — stacking a sticky tab bar under the sticky top nav eats too
much vertical space on narrow viewports. At lg+ the mobile top nav is
hidden, so we promote this header to sticky at top:0 there. */
position: relative;
z-index: 20;
display: flex;
align-items: center;
/* On mobile the eyebrow is hidden, so the tab pill bar is the only child —
center it. sm+ overrides back to space-between once the eyebrow shows. */
justify-content: center;
gap: 1.5rem;
padding: 0.9rem 1rem;
border-bottom: 1px solid transparent;
background: color-mix(in srgb, var(--background) 82%, transparent);
backdrop-filter: blur(18px) saturate(1.15);
-webkit-backdrop-filter: blur(18px) saturate(1.15);
border-bottom-color: color-mix(in srgb, var(--border) 55%, transparent);
transition:
background var(--duration-normal) var(--ease-default),
backdrop-filter var(--duration-normal) var(--ease-default),
border-color var(--duration-normal) var(--ease-default);
}
@media (min-width: 1024px) {
.library-marquee-header {
position: sticky;
top: 0;
z-index: 30;
}
}
.library-marquee-header.is-overlay {
background: transparent;
backdrop-filter: none;
-webkit-backdrop-filter: none;
border-bottom-color: transparent;
}
.library-marquee-header.is-overlay[data-scrolled="true"] {
background: color-mix(in srgb, var(--background) 72%, transparent);
backdrop-filter: blur(18px) saturate(1.15);
-webkit-backdrop-filter: blur(18px) saturate(1.15);
border-bottom-color: color-mix(in srgb, var(--border) 50%, transparent);
}
@media (min-width: 640px) {
.library-marquee-header {
justify-content: space-between;
padding: 1rem 1.5rem;
}
}
@media (min-width: 1024px) {
.library-marquee-header {
padding: 1.1rem 2.5rem;
}
}
@media (min-width: 1280px) {
.library-marquee-header {
padding: 1.1rem 3rem;
}
}
/* Small-caps tracked eyebrow label used above hero content
and in the marquee header as the page identifier. */
.hero-eyebrow {
font-family: var(--font-body);
font-size: 0.72rem;
font-weight: 600;
line-height: 1;
letter-spacing: 0.22em;
text-transform: uppercase;
color: color-mix(in srgb, var(--foreground) 62%, transparent);
white-space: nowrap;
}
.hero-eyebrow .hero-eyebrow-divider {
display: inline-block;
margin: 0 0.55em;
opacity: 0.45;
}
.hero-eyebrow .hero-eyebrow-strong {
color: color-mix(in srgb, var(--foreground) 92%, transparent);
}
.is-overlay .hero-eyebrow {
color: color-mix(in srgb, #fff 70%, transparent);
text-shadow: 0 2px 12px rgb(0 0 0 / 0.5);
}
.is-overlay .hero-eyebrow .hero-eyebrow-strong {
color: #fff;
}
/* Middot-separated metadata track for hero content. Replaces
chip-style badges with editorial cover-copy feel. */
.hero-meta-track {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0 0.1rem;
font-family: var(--font-body);
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.02em;
color: color-mix(in srgb, var(--foreground) 78%, transparent);
}
.hero-meta-track > span {
display: inline-block;
}
.hero-meta-track > span + span::before {
content: "·";
display: inline-block;
margin: 0 0.6rem;
opacity: 0.55;
color: color-mix(in srgb, var(--foreground) 55%, transparent);
}
/* Editorial slide indicator (counter + rail). */
.hero-slide-counter {
display: inline-flex;
align-items: center;
gap: 0.75rem;
font-family: var(--font-mono);
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.12em;
color: color-mix(in srgb, #fff 68%, transparent);
text-shadow: 0 2px 10px rgb(0 0 0 / 0.45);
}
.hero-slide-counter .num {
color: #fff;
}
.hero-slide-counter .sep {
width: 2.5rem;
height: 1px;
background: color-mix(in srgb, #fff 35%, transparent);
}
.hero-progress-rail {
position: relative;
width: 9rem;
height: 2px;
background: color-mix(in srgb, #fff 18%, transparent);
overflow: hidden;
border-radius: 9999px;
}
.hero-progress-rail > span {
position: absolute;
inset: 0;
background: color-mix(in srgb, #fff 88%, transparent);
transform-origin: left center;
transform: scaleX(0);
animation: hero-progress 8s linear forwards;
}
.hero-progress-rail[data-paused="true"] > span {
animation-play-state: paused;
}
@keyframes hero-progress {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
@media (prefers-reduced-motion: reduce) {
.hero-progress-rail > span {
animation: none;
transform: scaleX(1);
}
}
/* Pill-style tab bar used inside the marquee header. */
.marquee-tab-bar {
display: inline-flex;
align-items: center;
gap: 0.15rem;
padding: 0.25rem;
border-radius: 9999px;
background: color-mix(in srgb, #fff 8%, transparent);
border: 1px solid color-mix(in srgb, #fff 10%, transparent);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.library-marquee-header:not(.is-overlay) .marquee-tab-bar {
background: color-mix(in srgb, var(--foreground) 6%, transparent);
border-color: color-mix(in srgb, var(--border) 60%, transparent);
}
.marquee-tab-trigger {
display: inline-flex;
align-items: center;
padding: 0.5rem 1rem;
border-radius: 9999px;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.01em;
color: color-mix(in srgb, var(--foreground) 62%, transparent);
background: transparent;
border: none;
cursor: pointer;
white-space: nowrap;
transition: color var(--duration-fast) var(--ease-default);
}
.library-marquee-header.is-overlay .marquee-tab-trigger {
color: color-mix(in srgb, #fff 68%, transparent);
}
.marquee-tab-trigger:hover {
color: var(--foreground);
}
.library-marquee-header.is-overlay .marquee-tab-trigger:hover {
color: #fff;
}
.marquee-tab-trigger[data-state="active"] {
color: var(--foreground);
background: color-mix(in srgb, var(--foreground) 12%, transparent);
}
.library-marquee-header.is-overlay .marquee-tab-trigger[data-state="active"] {
color: #0a0a0b;
background: color-mix(in srgb, #fff 92%, transparent);
}
.marquee-tab-trigger:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
.panel-border {
border: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.04),
0 18px 45px -30px rgb(0 0 0 / 0.55);
}
.page-shell {
width: 100%;
max-width: 1400px;
margin-inline: auto;
padding-inline: 1rem;
}
.page-shell-wide {
width: 100%;
max-width: 1520px;
margin-inline: auto;
padding-inline: 1rem;
}
@media (min-width: 640px) {
.page-shell,
.page-shell-wide {
padding-inline: 1.5rem;
}
}
@media (min-width: 1024px) {
.page-shell,
.page-shell-wide {
padding-inline: 2.5rem;
}
}
@media (min-width: 1280px) {
.page-shell-wide {
padding-inline: 3rem;
}
}
.page-header {
display: flex;
flex-wrap: wrap;
align-items: end;
justify-content: space-between;
gap: 1rem;
}
.page-title {
font-size: clamp(2rem, 4vw, 3.75rem);
line-height: 0.95;
font-weight: 800;
letter-spacing: -0.05em;
}
.page-subtitle {
max-width: 60ch;
color: color-mix(in srgb, var(--foreground) 68%, var(--muted-foreground));
line-height: 1.7;
}
.surface-panel {
background: color-mix(in srgb, var(--surface) 90%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
border-radius: calc(var(--radius-2xl) + 0.35rem);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.04),
0 24px 50px -34px rgb(0 0 0 / 0.65);
}
.surface-panel-subtle {
background: color-mix(in srgb, var(--surface) 72%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 62%, transparent);
border-radius: calc(var(--radius-xl) + 0.25rem);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.04);
}
.caption-empty-state span {
display: block;
height: 0.35rem;
border-radius: 999px;
background: color-mix(in srgb, var(--foreground) 18%, transparent);
}
.caption-empty-state span:nth-child(1) {
width: 72%;
margin-inline: auto;
}
.caption-empty-state span:nth-child(2) {
width: 54%;
margin-inline: auto;
}
.caption-empty-state span:nth-child(3) {
width: 38%;
margin-inline: auto;
}
/* ── Scan Queue Accent ──────────────────────────────────── */
/* Animated gradient sweep across the top of the scan queue
to signal live activity. Only visible when scans are active. */
.scan-queue-accent {
background: linear-gradient(
90deg,
transparent 0%,
color-mix(in srgb, var(--primary) 50%, transparent) 45%,
var(--primary) 50%,
color-mix(in srgb, var(--primary) 50%, transparent) 55%,
transparent 100%
);
background-size: 200% 100%;
animation: scan-accent-sweep 4s ease-in-out infinite;
opacity: 0.6;
}
@keyframes scan-accent-sweep {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.auth-shell {
min-height: 100dvh;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
}
.auth-shell::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(
circle at 18% 18%,
color-mix(in srgb, var(--primary) 18%, transparent),
transparent 28%
),
radial-gradient(
circle at 82% 12%,
color-mix(in srgb, var(--ambient) 15%, transparent),
transparent 26%
),
linear-gradient(
135deg,
color-mix(in srgb, var(--background) 96%, transparent),
var(--background)
);
pointer-events: none;
}
.auth-card {
position: relative;
z-index: 1;
width: 100%;
max-width: 28rem;
border-radius: 2rem;
padding: 1.75rem;
}
.admin-shell {
width: 100%;
min-width: 0;
max-width: 1560px;
margin-inline: auto;
}
/* AdminLayout owns the outer gutter. Older admin pages still use page-shell
for their max-width, so remove its second horizontal gutter when nested. */
.admin-shell > .page-shell,
.admin-shell > .page-shell-wide {
padding-inline: 0;
}
.admin-dashboard-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 10.5rem), 1fr));
gap: 0.875rem;
}
@media (max-width: 639px) {
.admin-shell .page-header {
align-items: stretch;
}
.admin-shell .page-header > * {
width: 100%;
min-width: 0;
}
.admin-shell .page-header > :not(:first-child) {
flex-wrap: wrap;
justify-content: flex-start;
text-align: left;
}
.admin-shell .page-header [data-slot="button"] {
min-height: 2.75rem;
}
}
/* ── Section Header ──────────────────────────────────────── */
/* Consistent section heading with optional icon.
Usage: */
.section-header {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--foreground);
padding-bottom: 0.75rem;
}
/* ── Live Badge ──────────────────────────────────────────── */
.live-badge {
margin-left: auto;
padding: 2px 8px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
background: rgba(255, 60, 60, 0.12);
color: #f87171;
border: 1px solid rgba(255, 60, 60, 0.2);
animation: pulse-opacity 2s ease-in-out infinite;
}
/* ── Progress Bar ────────────────────────────────────────── */
/* Thin progress indicator for watch progress on cards/heroes.
Usage: */
.progress-bar {
width: 100%;
height: 3px;
background-color: color-mix(in srgb, var(--foreground) 15%, transparent);
border-radius: 9999px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background-color: var(--primary);
border-radius: 9999px;
transition: width var(--duration-normal) var(--ease-default);
}
/* ── Watched Badge ───────────────────────────────────────── */
/* Checkmark badge overlay on fully-watched media cards.
Position: absolute top-right of card.
Usage: */
.watched-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 9999px;
background-color: var(--primary);
color: var(--primary-foreground);
}
/* ── Play Overlay ────────────────────────────────────────── */
/* Semi-transparent play button centered on thumbnails.
Usage: */
.play-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--background) 40%, transparent);
border-radius: inherit;
opacity: 0;
transition: opacity var(--duration-normal) var(--ease-default);
}
.group:hover .play-overlay,
.play-overlay:hover {
opacity: 1;
}
/* ── Impersonation Banner ────────────────────────────────── */
/* Override strip shown while an admin is viewing the app as
another user. Warm-gold accents on a glass-dark surface —
unmistakable without being alarming. Pinned to the viewport
top via Tailwind's `sticky top-0 z-50` utilities on the
outer element. */
/* Global sidebar offset used by out-of-tree chrome that needs to
align with the current sidebar width. Layout sets
`data-sidebar-collapsed` on the root element; this media-query
block resolves the correct px value per breakpoint/state. */
:root {
--app-sidebar-offset: 0px;
}
@media (min-width: 1024px) {
:root {
--app-sidebar-offset: 260px;
}
:root[data-sidebar-collapsed="true"] {
--app-sidebar-offset: 64px;
}
}
.impersonation-banner {
--impersonation-accent: oklch(0.82 0.14 78);
--impersonation-accent-glow: oklch(0.82 0.14 78 / 0.55);
--impersonation-accent-soft: oklch(0.82 0.14 78 / 0.12);
--impersonation-accent-line: oklch(0.82 0.14 78 / 0.32);
/* Snaps straight to the destination so the document lays out once; the
compensation transform below carries the edge across. Animating this
margin instead would re-run layout on every frame. */
margin-left: var(--app-sidebar-offset);
position: sticky;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--background) 94%, transparent),
color-mix(in srgb, var(--background) 88%, transparent)
);
backdrop-filter: blur(20px) saturate(1.3);
-webkit-backdrop-filter: blur(20px) saturate(1.3);
border-bottom: 1px solid color-mix(in srgb, var(--impersonation-accent) 16%, var(--border));
box-shadow:
inset 0 1px 0 color-mix(in srgb, var(--impersonation-accent) 22%, transparent),
0 10px 32px -20px rgb(0 0 0 / 0.65);
}
/* The banner renders above the routes, outside Layout, so it tracks the
sidebar through root attributes rather than the props `.sidebar-main-stage`
reads in-tree. The mechanism is identical: the margin above snaps to the
destination, and an inverse transform holds the previous visual offset for
one frame before releasing to zero on the same state change that moves the
sidebar — so both edges travel together on the compositor.
Without this the banner jumped instantly while the sidebar took 300ms, and
since it is `z-50` against the sidebar's `z-40`/`45` it painted over the
logo on the way in and left a gap on the way out.
Breakpoint deliberately matches `--app-sidebar-offset` (1024px) rather than
Tailwind's `lg` (64rem): `html[data-text-scale]` re-bases rem, so at large
text those two diverge and the compensation must follow whichever query
actually changes the margin it is compensating for. */
/* While the banner holds its previous offset it is translated right, so its
right edge sits briefly past the viewport. Layout already clips its own
subtree this way; the banner is mounted above it, so the app root needs the
same treatment or that overhang becomes a document-level horizontal scroll
for the length of the transition. `clip` rather than `hidden` so this never
becomes a scroll container and the banner's `position: sticky` keeps
resolving against the viewport. */
#root {
overflow-x: clip;
}
@media (min-width: 1024px) {
.impersonation-banner {
transition: transform var(--duration-sidebar-collapse) var(--ease-sidebar-collapse);
}
:root[data-sidebar-collapsed="true"]:not([data-sidebar-visual-collapsed="true"])
.impersonation-banner {
transform: translateX(196px);
transition: none;
}
:root:not([data-sidebar-collapsed="true"])[data-sidebar-visual-collapsed="true"]
.impersonation-banner {
transform: translateX(-196px);
transition: none;
}
}
/* Luminous top accent line — fades in from both edges so the
banner reads as a deliberate strip rather than a hard bar. */
.impersonation-banner::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
var(--impersonation-accent-line) 18%,
var(--impersonation-accent-line) 82%,
transparent
);
pointer-events: none;
}
/* Soft warm wash on the left edge so the eye lands on the
status dot first. Fades out before the button column. */
.impersonation-banner::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--impersonation-accent) 10%, transparent),
transparent 38%
);
pointer-events: none;
}
.impersonation-banner-inner {
position: relative;
z-index: 1;
}
.impersonation-dot {
background: var(--impersonation-accent);
box-shadow: 0 0 10px 1px var(--impersonation-accent-glow);
}
.impersonation-pulse {
background: var(--impersonation-accent);
opacity: 0.55;
}
.impersonation-label {
letter-spacing: 0.22em;
color: color-mix(in srgb, var(--impersonation-accent) 85%, var(--foreground));
}
.impersonation-chip {
background: var(--impersonation-accent-soft);
color: color-mix(in srgb, var(--impersonation-accent) 85%, var(--foreground));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--impersonation-accent) 32%, transparent);
}
.impersonation-end-btn {
color: var(--foreground);
background: color-mix(in srgb, var(--foreground) 5%, transparent);
border: 1px solid color-mix(in srgb, var(--impersonation-accent) 24%, var(--border));
}
.impersonation-end-btn:hover {
background: color-mix(in srgb, var(--impersonation-accent) 14%, transparent);
border-color: color-mix(in srgb, var(--impersonation-accent) 55%, var(--border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--impersonation-accent) 8%, transparent);
}
.impersonation-end-btn:focus-visible {
border-color: var(--impersonation-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--impersonation-accent) 28%, transparent);
}
}