/* ============================================================
   veilchen.tv — Shared Design System
   Extracted from index - Copy.html (reference)
   ============================================================ */

/* Primary tokens (--bg/--panel/--line/--text/--accent/--shade) are the only ones any page
   ever overrides directly — a channel page's own owner theme (plugins/user-pages) or a
   logged-in viewer's global theme preference (utils.js's applyGlobalThemePreference()),
   both by setting these as inline custom properties on <html>. Every other token below is
   derived from those via color-mix() rather than pinned to its own hex, so the whole
   design system recomputes correctly under either kind of override on any page — not just
   the channel page, which is why these live here instead of as a page-local override.
   Each ratio reproduces this file's own original hardcoded hex for that token, computed
   from the matching default base color it's now derived from (e.g. --sunken's 62% of the
   default --bg reproduces the old #1A1A1A exactly), so a page with no override at all is
   visually identical to before.
   --shade is the "recede toward" direction used by every *-dim/-faint token: black for a
   dark theme (the default), white for a light one (see THEME_PRESETS' light entries in
   utils.js) — mixing dark theme text/surfaces toward black dims them correctly, but doing
   the same to a light theme's already-dark text/light surfaces would darken them further
   instead of receding them toward the page background, which is backwards.
   --sunken/--panel-2 are NOT safe to derive from --shade the same way, and don't try to
   "fix" them by making them shade-based — a recessed surface (form fields, thumbs, code
   blocks) and a hover highlight need to move AWAY from the page background regardless of
   whether the theme is light or dark, but --shade only encodes "which direction is the
   background," not "which direction reads as recessed/highlighted." For a dark theme those
   happen to be the same direction (both mixing toward black), so the formula below works —
   but for a light theme they're opposite (bg is already at/near white, so mixing --sunken
   toward --shade=white collapses to bg itself; --panel-2 hardcodes "toward white" for the
   same reason, which is exactly backwards once panel is already near-white). Rather than
   chase a single formula that works in both directions, light presets in THEME_PRESETS set
   `sunken`/`panel2` as explicit hand-picked overrides (see githublight/rosepinedawn below) —
   applyThemeColors() only touches --sunken/--panel-2 when a preset actually sets them, so
   every other preset keeps using the formula below unchanged.
   --accent-ink is the text color for solid-accent-background UI (buttons, ::selection) —
   black by default since every current accent is light/pastel enough for it, but a preset
   with a more saturated, darker accent (githublight's `#0969da`) can override it to white
   where black text would dip under WCAG AA contrast. */
:root {
  --bg: #2A2A2A;
  --panel: #333333;
  --panel-2: color-mix(in srgb, var(--panel) 97%, white);
  --shade: black;
  --sunken: color-mix(in srgb, var(--bg) 62%, var(--shade));   /* recessed surface: form fields, thumbs, code blocks */
  --line: #4A4A4A;
  --line-dim: color-mix(in srgb, var(--line) 81%, var(--shade));
  --text: #E7E7E2;
  --text-dim: color-mix(in srgb, var(--text) 66%, var(--shade));
  --text-faint: color-mix(in srgb, var(--text) 46%, var(--shade));
  --accent: #4ADE80;
  --accent-ink: black;   /* text color on solid --accent backgrounds (buttons, ::selection) */
  --accent-dim: color-mix(in srgb, var(--accent) 55%, var(--shade));
  --accent-glow: color-mix(in srgb, var(--accent) 13%, transparent);
  --accent-bright: color-mix(in srgb, var(--accent) 80%, white);   /* seek-bar hover highlight */
  --danger: #EF4444;
  --discord: #8EA0FF;
  --youtube: #ff0000;   /* external brand mark, same treatment as --discord — constant across themes, not derived */
  --font: 'JetBrains Mono', monospace;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { min-height: 100%; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 13px;
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--accent); color: var(--accent-ink); }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; border: none; background: none; color: inherit; }

/* ---------- LINK (shared) ----------
   The border-reveals-on-hover mechanic already used across the UI's interactive
   chrome (.nav-tab, .icon-btn, .btn) — border is always reserved at 1px solid
   transparent so hovering never shifts layout, it just reveals color. Plain
   navigation/identity links (streamer names, breadcrumbs, "back to X", inline
   actions) opt into just this mechanic; each keeps its own resting/hover color. */
.link { border: 1px solid transparent; border-radius: 2px; transition: border-color .15s; }
.link:hover { border-color: var(--line); }

::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line); border-radius: 0; }
::-webkit-scrollbar-thumb:hover { background: var(--accent-dim); }
:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }

/* ---------- APP SHELL ---------- */
/* dvh accounts for mobile browser chrome (the address/toolbar showing or hiding) that a
   plain vh doesn't — 100vh on a phone is sized against the *largest* possible viewport, so
   content sits partly behind the toolbar until it scrolls away. Declared after vh so
   browsers without dvh support (rare at this point) just keep the vh value. */
.app { display: flex; flex-direction: column; height: 100vh; height: 100dvh; overflow: hidden; }

/* ---------- MAIN CONTENT (scrollable) ---------- */
main { flex: 1; overflow-y: auto; }

/* ---------- BRACKETS ---------- */
.bracketed { position: relative; }
.bracketed::before, .bracketed::after,
.bracketed .bk-tr, .bracketed .bk-bl {
  content: ''; position: absolute; width: 9px; height: 9px; pointer-events: none;
  border-color: var(--line); opacity: .9;
}
.bracketed::before { top: -1px; left: -1px; border-top: 1px solid; border-left: 1px solid; }
.bracketed::after  { bottom: -1px; right: -1px; border-bottom: 1px solid; border-right: 1px solid; }
.bracketed .bk-tr { top: -1px; right: -1px; border-top: 1px solid var(--line); border-right: 1px solid var(--line); }
.bracketed .bk-bl { bottom: -1px; left: -1px; border-bottom: 1px solid var(--line); border-left: 1px solid var(--line); }

/* ---------- TOP BAR (harmonized) ---------- */
.topbar {
  height: 52px; min-height: 52px;
  display: flex; align-items: center; gap: 18px;
  padding: 0 18px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: relative; z-index: 40;
}

.logo {
  display: flex; align-items: center; gap: 8px;
  font-weight: 700; font-size: 14px; letter-spacing: .5px;
  flex-shrink: 0;
}
.logo:hover { color: var(--accent); }
.logo .glyph { color: var(--accent); }
.logo .dim   { color: var(--text-faint); font-weight: 400; }

/* ---------- VERTICAL DIVIDER ---------- */
.divider-v { width: 1px; align-self: stretch; margin: 12px 0; background: var(--line); flex-shrink: 0; }

/* ---------- NAV TABS ---------- */
.nav-tabs { display: flex; gap: 2px; }
.nav-tab {
  padding: 5px 12px; font-size: 11.5px; color: var(--text-dim);
  border: 1px solid transparent; font-weight: 500;
}
.nav-tab:hover { color: var(--text); border-color: var(--line); }
.nav-tab.active { color: var(--accent); border-bottom-color: var(--accent); }

/* ---------- ICON BUTTONS ---------- */
.icon-btn {
  width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
  color: var(--text-dim); border: 1px solid transparent; position: relative;
}
.icon-btn:hover { color: var(--text); border-color: var(--line); }
.icon-btn svg { width: 16px; height: 16px; }
/* Profiles directory icon hidden site-wide — the topbar button that opens the public
   profiles dropdown, not the feature itself (GET /api/users/public, the "Public profile"
   toggle in Account Settings, and direct /u/:username links all still work). */
.icon-btn[title="Profiles"] { display: none; }

/* ---------- TOP RIGHT (shared) ---------- */
.top-right{display:flex;gap:14px;margin-left:auto;}
.user-pill{padding:5px 12px;border:1px solid var(--line);display:flex;gap:8px;align-items:center;font-size:12px;}


.d-item{padding:10px 12px;font-size:12px;border-bottom:1px solid var(--line);display:block;text-align:left;width:100%;cursor:pointer;background:none;color:inherit;font-family:inherit;}
.d-item:hover{background:var(--panel-2);color:var(--accent)}
.bell-badge{position:absolute;top:-2px;right:-4px;background:var(--danger);color:white;font-size:9px;font-weight:bold;min-width:15px;height:15px;border-radius:8px;display:none;align-items:center;justify-content:center;padding:0 3px;text-align:center;}
.bell-badge.visible{display:flex;}

/* ---------- CUSTOM EMOJI (see js/emoji.js, lib/emoji.js) ---------- */
/* Sized in em so an emoji tracks whatever text it sits in — chat, a comment, or the
   overlay's much larger type — without a per-surface rule. */
.emoji{height:1.45em;width:auto;vertical-align:-.3em;object-fit:contain;}
.emoji-picker{
  position:fixed;z-index:120;min-width:180px;max-width:240px;
  background:var(--panel);border:1px solid var(--line);
  box-shadow:0 8px 24px rgba(0,0,0,.45);padding:4px 0;
}
.emoji-picker[hidden]{display:none;}
.emoji-opt{
  display:flex;align-items:center;gap:8px;width:100%;padding:5px 10px;
  background:none;border:none;color:var(--text-dim);font-family:inherit;font-size:11.5px;
  cursor:pointer;text-align:left;
}
.emoji-opt img{height:20px;width:20px;object-fit:contain;flex-shrink:0;}
.emoji-opt.active,.emoji-opt:hover{background:var(--panel-2);color:var(--accent);}

/* ---------- NOTIFICATION BELL ITEMS (see js/notifications.js) ---------- */
.notif-item{display:flex;align-items:flex-start;gap:8px;}
.notif-body{flex:1;min-width:0;}
.notif-sub{display:block;font-size:10px;color:var(--text-dim);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.notif-time{font-size:10px;color:var(--text-dim);flex-shrink:0;}
/* The live dot doubles as the unread marker — an entry only exists while its stream does,
   so "still live" and "not seen yet" are the two things worth showing, in one place. */
.notif-live{width:7px;height:7px;border-radius:50%;background:var(--text-dim);flex-shrink:0;margin-top:5px;}
.notif-item.unread .notif-live{background:var(--danger);box-shadow:0 0 0 3px rgba(239,68,68,.18);}
.notif-empty{padding:14px;color:var(--text-dim);font-size:11.5px;text-align:center;}

/* ---------- DROPDOWN (shared) ---------- */
.dropdown-wrap { position: relative; }
.drop-menu, .dropdown {
  position: absolute; top: calc(100% + 8px); right: 0; min-width: 220px;
  max-height: 320px; overflow-y: auto;
  background: var(--panel); border: 1px solid var(--line);
  z-index: 60; opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity .12s ease, transform .12s ease, visibility .12s;
}
.drop-menu.open, .dropdown.open { opacity: 1; visibility: visible; transform: translateY(0); }

/* ---------- BUTTONS (shared) ---------- */
.btn {
  font-size: 12px; font-weight: 600; padding: 7px 14px; border: 1px solid var(--line);
  letter-spacing: .2px;
}
.btn:hover { border-color: var(--accent-dim); color: var(--accent); }
.btn-solid { background: var(--accent); color: var(--accent-ink); border-color: var(--accent); }
.btn-solid:hover { color: var(--accent-ink); opacity: .85; }

/* ---------- FORM FIELDS (shared) ----------
   One canonical field look for every text input / select / textarea in the app.
   Pages contribute only their own sizing and layout deltas (width, padding,
   resize, min-height) — never the colors, border or focus state. */
.modal input,
.s-field input,
.search-box, .filter-select,
.export-input, .export-select,
.comment-form textarea, .comment-textarea {
  background: var(--sunken);
  border: 1px solid var(--line);
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  outline: none;
  transition: border-color .2s;
}
.modal input:focus,
.s-field input:focus,
.search-box:focus, .filter-select:focus,
.export-input:focus, .export-select:focus,
.comment-form textarea:focus, .comment-textarea:focus {
  border-color: var(--accent);
}

/* ---------- ACTION BUTTONS (shared) ----------
   Used on VOD/clip cards (vods.html) and the VOD detail page (vod-player.html).
   Those pages set their own font-size/padding/background on top. */
.action-btn { border: 1px solid var(--line); color: var(--text-dim); transition: all .2s; }
.action-btn:hover { border-color: var(--accent); color: var(--text); }
.action-btn.delete:hover { border-color: var(--danger); color: var(--danger); }

/* ---------- CARD OVERFLOW MENU (shared) ----------
   The "…" menu on VOD cards and on the VOD detail page. Pages set only the
   dropdown's anchor edge (top/bottom), min-width and type scale. */
.card-menu-wrap { position: relative; margin-left: auto; }
.menu-dots-btn { background: none; color: #888; line-height: 1; cursor: pointer; transition: all .2s; }
.menu-dots-btn:hover { color: white; }
.card-menu-dropdown {
  position: absolute; right: 0; z-index: 50; display: none;
  background: var(--panel); border: 1px solid var(--line);
}
.card-menu-dropdown.open { display: block; }
.card-menu-dropdown button, .card-menu-dropdown a {
  display: flex; width: 100%; align-items: center; text-align: left;
  background: none; border: none; color: var(--text-dim);
  font-family: inherit; transition: all .15s; cursor: pointer; box-sizing: border-box;
}
.card-menu-dropdown button:hover, .card-menu-dropdown a:hover { background: #2A2A2A; color: white; }
.card-menu-dropdown button.danger:hover { color: var(--danger); background: rgba(239,68,68,.08); }
.card-menu-dropdown svg {
  flex-shrink: 0; stroke: currentColor; fill: none;
  stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
}

/* ---------- LOADING PLACEHOLDER (shared) ---------- */
.loading { color: var(--text-dim); }

/* ---------- EMPTY STATE (shared) ---------- */
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-faint); }
.empty-state svg { width: 48px; height: 48px; margin-bottom: 12px; opacity: .3; stroke: var(--text-faint); }

/* ---------- MEDIA CARD / GRID (shared) ----------
   The VOD/clip card used by both the VOD grid (vods.html) and a channel page's
   VODs/clips subsections (user.html). Pages set their own grid breakpoints
   and whichever extra badges/rows they need (streamer row, YouTube badge, ...) —
   this is just the card shell every consumer relies on. */
.vod-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.vod-card { background: var(--panel); border: 1px solid var(--line); cursor: pointer; transition: border-color .2s; }
.vod-card:hover { border-color: var(--accent); }
.vod-thumb { position: relative; width: 100%; aspect-ratio: 16/9; background: var(--sunken); display: flex; align-items: center; justify-content: center; overflow: hidden; }
.vod-thumb img { width: 100%; height: 100%; object-fit: cover; }
.vod-thumb svg { opacity: .3; } /* size comes from each placeholder <svg>'s own width/height attrs */
.play-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,.3); opacity: 0; transition: opacity .2s; }
.vod-card:hover .play-overlay { opacity: 1; }
.play-overlay svg { width: 48px; height: 48px; fill: none; stroke: white; stroke-width: 1.5; }
.vod-duration { position: absolute; bottom: 6px; right: 6px; background: rgba(0,0,0,.8); color: white; font-size: 10px; padding: 2px 5px; border-radius: 2px; }
.vod-info { padding: 10px 12px; }
.vod-title { font-size: 12.5px; font-weight: 700; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vod-meta { display: flex; justify-content: space-between; font-size: 10px; color: #888; margin-bottom: 6px; }
.vod-actions { display: flex; gap: 4px; align-items: center; margin-top: 6px; }

/* ---------- MODAL (shared) ---------- */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.active { display: flex; }
.modal { background: var(--panel); border: 1px solid var(--line); padding: 24px; width: 400px; max-width: 90vw; }
.modal h3 { margin-bottom: 16px; font-size: 14px; }
.modal input { width: 100%; padding: 8px 12px; margin-bottom: 16px; }
/* Those three are written for the modal's text fields, and a checkbox is not one: full width
   plus padding turns a 13px control into a block that fills the modal and squeezes whatever
   label sits beside it down to a one-character column — worst on a phone, but wrong at every
   width. Reset here rather than in the one page that has such a checkbox today, since the rule
   above is what's making the assumption. */
.modal input[type=checkbox] { width: auto; padding: 0; margin: 0; accent-color: var(--accent); }
.modal-btns { display: flex; gap: 8px; justify-content: flex-end; }
.btn-primary { background: var(--accent); color: var(--accent-ink); padding: 8px 16px; font-size: 12px; font-weight: 600; border: none; cursor: pointer; }
.btn-secondary { background: none; border: 1px solid var(--line); color: var(--text); padding: 8px 16px; font-size: 12px; cursor: pointer; }

/* ---------- OVERLAY + DRAWER (shared) ---------- */
.overlay-fix, .overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 80; opacity: 0; visibility: hidden; transition: opacity .2s ease, visibility .2s; }
.overlay.open, .overlay-fix.open { opacity: 1; visibility: visible; }

/* ---------- SETTINGS DRAWER (shared) ---------- */
.settings-drawer{position:fixed;top:0;right:0;bottom:0;width:350px;background:var(--panel);border-left:1px solid var(--line);transform:translateX(100%);transition:.2s;z-index:90;display:flex;flex-direction:column;}
.drawer-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid var(--line);}
.drawer-tabs{display:flex;border-bottom:1px solid var(--line);}
/* Underline-style tab button — used by the settings drawer (Account/Stream) and by the
   channel page's subsection tabs (user.html). .drawer-tabs' flex:1 stretch is drawer-only
   layout; consumers that don't want it override flex/text-align on top of this. */
.tab-btn{flex:1;padding:10px;font-size:11.5px;font-weight:600;text-align:center;color:var(--text-dim);border:none;cursor:pointer;border-bottom:2px solid transparent;font-family:inherit;background:none;}
.tab-btn:hover{color:var(--text);}
.tab-btn.active{color:var(--accent);border-color:var(--accent);}
.tab-content{flex:1;overflow-y:auto;display:none;padding:20px;}
.tab-content.active{display:block;}

/* Settings field styles */
.s-field{margin-bottom:14px}
.s-field input{width:calc(100% - 2px);box-sizing:border-box;margin-bottom:0;padding:8px}
.btn-save-profile{background:#2a5a3f;color:#fff;border:none;padding:8px 16px;font-size:12px;font-weight:600;cursor:pointer;margin-top:4px;width:100%}
.btn-save-profile:hover{background:#3a7a55}

/* Toggle switch */
.toggle-row{display:flex;align-items:center;justify-content:space-between;}
.tog-wrap{position:relative;display:inline-block;width:42px;height:22px;flex-shrink:0}
.tog-wrap input{opacity:0;width:0;height:0}
.tog-slider{position:absolute;inset:0;background:#3a3a3a;border-radius:11px;cursor:pointer;transition:.2s}
.tog-slider:before{content:"";position:absolute;height:16px;width:16px;left:3px;top:3px;background:#888;border-radius:50%;transition:.2s}
.tog-wrap input:checked + .tog-slider{background:#2a5a3f}
.tog-wrap input:checked + .tog-slider:before{transform:translateX(20px);background:var(--accent)}

/* OBS Config in Settings */
.rtmp-config{background:var(--sunken);padding:12px;border:1px dashed #444;margin-bottom:20px;}
.rtmp-row{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:white;margin-bottom:8px;}
.copy-btn{color:var(--accent);text-decoration:underline;cursor:pointer;background:none;}
.key-display{font-family:'JetBrains Mono';width:100%;font-size:10px;color:#ccc;word-break:break-all;background:#2A2A2A;border:1px solid #333;padding:6px;user-select:all;}

/* Avatar upload in settings */
.avatar-upload-row{display:flex;align-items:center;gap:14px;margin-bottom:18px}
.avatar-preview{width:52px;height:52px;border-radius:50%;background:var(--sunken);border:1px solid var(--line);display:flex;align-items:center;justify-content:center;font-size:20px;color:#888;font-weight:bold;overflow:hidden;flex-shrink:0}
.avatar-preview img{width:100%;height:100%;object-fit:cover;}
.avatar-actions{display:flex;flex-direction:column;gap:4px}

/* Settings responsive */
@media(max-width:540px){.settings-drawer{width:100vw}}

/* ---------- TOAST (shared) ---------- */
.toast-container { position: fixed; top: 62px; right: 20px; z-index: 150; display: flex; flex-direction: column; gap: 8px; pointer-events: none; }
.toast { background: var(--panel); border: 1px solid var(--line); padding: 12px 16px; width: 300px; color: var(--text); font-size: 12px; opacity: 0; transform: translateX(40px); transition: .3s ease; pointer-events: auto; display: flex; gap: 10px; }
.toast.show { opacity: 1; transform: none; }
.toast-icon{color:var(--accent);font-size:18px;flex-shrink:0;margin-top:-2px;line-height:1;}
.toast-body{flex:1}.toast-body strong{display:block;margin-bottom:2px;color:var(--text);font-size:13px;}.toast-close{background:none;color:var(--text-faint);font-size:16px;border:none;cursor:pointer;flex-shrink:0;padding:2px 4px;}.toast-close:hover{color:var(--text)}

/* ---------- PLAYER CONTROLS (shared) ---------- */
.player-wrapper{position:relative;width:100%;flex-shrink:0;aspect-ratio:16/9;background:#000;border:1px solid var(--line);overflow:hidden;}
.player-wrapper video{width:100%;height:100%;display:block;object-fit:contain;}

/* Center overlay for offline/loading states */
.p-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:20;transition:opacity .3s ease;}

/* Top overlay (live pill, view count) */
.p-overlay{position:absolute;top:10px;left:10px;right:10px;display:flex;justify-content:space-between;z-index:20;}
.live-pill{border:1px solid var(--accent-dim);color:var(--accent);font-size:10px;padding:4px 8px;font-weight:bold;letter-spacing:.5px;display:inline-flex;align-items:center;gap:6px;background:rgba(0,0,0,.7);}
.stat-pill{font-size:10px;color:#ccc;background:rgba(0,0,0,.7);border:1px solid #333;padding:4px 8px;}

/* Bottom controls bar */
.player-controls{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(transparent,rgba(0,0,0,.85));padding:30px 12px 10px;display:flex;align-items:center;gap:10px;z-index:25;opacity:0;transition:opacity .2s ease;}
.player-wrapper:hover .player-controls{opacity:1;}
/* Touch devices have no persistent :hover, so a hover-only reveal means the
   controls bar (and everything in it) is effectively unreachable on mobile. */
@media (hover:none){ .player-controls{opacity:1;} }

/* Control buttons */
.pc-btn{background:none;border:none;color:#fff;cursor:pointer;display:flex;align-items:center;padding:4px;flex-shrink:0;}
.pc-btn svg{width:20px;height:20px;fill:currentColor}
.pc-btn:hover{color:var(--accent)}
/* Playback-speed button: accented, with the rate spelled out, whenever it isn't 1x. */
.pc-btn.rate-active{color:var(--accent)}
.pc-rate{font-size:10px;font-weight:700;letter-spacing:.3px;line-height:1;}
.pc-rate:not(:empty){margin-left:3px;}

/* Volume controls — touch-action:none stops a touchstart on the bar from being
   read as a page-scroll gesture, which would otherwise fight the pointer drag
   wired in player-controls.js's wireDragBar. */
.vol-wrap{display:flex;align-items:center;gap:6px}
.custom-vol-bar{width:70px;height:3px;background:#444;border-radius:2px;cursor:pointer;position:relative;flex-shrink:0;touch-action:none;}
.custom-vol-fill{height:100%;background:#fff;border-radius:2px;width:100%;}

/* Seek/progress bar (VOD) */
.seek-wrap{display:flex;align-items:center;flex:1;min-width:0;position:relative;height:16px;}
.seek-bar{width:100%;height:3px;background:rgba(255,255,255,.2);border-radius:2px;cursor:pointer;position:absolute;top:50%;transform:translateY(-50%);touch-action:none;}
.seek-fill{height:100%;background:var(--accent);border-radius:2px;width:0;}
.seek-thumb{display:none;position:absolute;top:50%;width:13px;height:13px;background:var(--accent);border-radius:50%;transform:translate(-6px,-50%);z-index:2;pointer-events:none;}
.seek-bar:hover .seek-fill,.seek-bar.dragging .seek-fill{background:var(--accent-bright);}
.seek-bar:hover .seek-thumb,.seek-bar.dragging .seek-thumb{display:block;}

/* Time display */
.time-display{font-size:11px;color:#ccc;font-variant-numeric:tabular-nums;white-space:nowrap;flex-shrink:0;}

/* Speed menu (VOD) */
.speed-menu{position:absolute;bottom:46px;right:52px;background:rgba(0,0,0,.9);border:1px solid var(--line);border-radius:6px;padding:4px 0;z-index:50;display:none;min-width:90px;}
.speed-menu.open{display:block;}
.speed-opt{padding:6px 14px;font-size:12px;color:#ccc;cursor:pointer;text-align:center;border:none;background:none;width:100%;font-family:inherit;}
.speed-opt:hover,.speed-opt.active{color:var(--accent);background:rgba(255,255,255,.05);}

/* "Stats for nerds" panel (live players — see player-controls.js's wireStatsPanel) */
.stats-panel{position:absolute;bottom:46px;right:12px;background:rgba(0,0,0,.92);border:1px solid var(--line);padding:6px 10px;z-index:50;display:none;min-width:220px;max-width:calc(100vw - 24px);font-size:11px;}
.stats-panel.open{display:block;}
.stats-row{display:flex;justify-content:space-between;gap:16px;padding:2px 0;color:#999;white-space:nowrap;}
.stats-row span:last-child{color:#ddd;font-variant-numeric:tabular-nums;margin-left:auto;}

/* Youtube mode overrides */
.youtube-mode{background:#000 !important;border-color:var(--accent-dim);}
.youtube-mode iframe{width:100%;height:100%;position:absolute;inset:0;z-index:30;}
#yt-player-container{width:100% !important;height:calc(100% + 80px) !important;position:absolute;top:-50px;bottom:0;z-index:30;}
#yt-player-container iframe{width:100%;height:100%;}
.youtube-mode .p-overlay,.youtube-mode .p-center,.youtube-mode .player-noise{display:none;}
.youtube-mode #btnStats,.youtube-mode .stats-panel{display:none;} /* stats read from RTCPeerConnection, not the YT iframe */
.youtube-mode .player-controls {z-index:40; opacity:1;}

/* ---------- RESPONSIVE ---------- */
@media (max-width: 960px) {
  .topbar { flex-wrap: wrap; height: auto; padding: 10px 14px; align-items: center; gap: 8px; }
  .user-pill { min-height: 30px; display: flex; align-items: center; justify-content: center; box-sizing: border-box; }
  .divider-v { display: none; }

  /* chan-select → second row, centered */
  .chan-select { flex: none !important; min-width: 100% !important; width: 100% !important; justify-content: center; margin-top: 6px !important; order: 3; }

  /* Tighten player controls */
  .player-controls { padding: 20px 8px 6px; gap: 6px; }
  .custom-vol-bar { width: 50px; }
  .time-display { font-size: 10px; }

  /* Toast tighter */
  .toast-container { right: 10px; top: 58px; }
  .toast { width: 260px; padding: 10px 12px; }
}

@media (max-width: 768px) {
  /* Further tighten controls */
  .player-controls { gap: 4px; padding: 16px 6px 5px; }
  .custom-vol-bar { width: 40px; }
  .time-display { font-size: 9px; }
  .pc-btn svg { width: 18px; height: 18px; }

  /* Speed menu positioning */
  .speed-menu { right: 20px; bottom: 40px; min-width: 80px; }

  /* Toast smaller */
  .toast-container { right: 6px; top: 54px; }
  .toast { width: 240px; font-size: 11px; }

  .comment-form textarea { min-width: 0; font-size: 11px; }

  /* MEDIA LIST — row/card variants */
  .vlh-date,.vr-date{display:none}
  .vr-stats span:last-child{display:none}
  .vr-stats{width:70px}
  .vlh-stats{width:70px}
  .clip-grid{grid-template-columns:repeat(auto-fill, minmax(220px, 1fr)); gap:14px;}
}

@media (max-width: 540px) {
  /* Topbar compact */
  .topbar { padding: 8px 10px; gap: 8px; min-height: 44px; height: auto; }
  .logo { font-size: 13px; }
  .nav-tab { padding: 4px 8px; font-size: 10.5px; }
  .icon-btn { width: 26px; height: 26px; }
  .icon-btn svg { width: 14px; height: 14px; }
  .user-pill { padding: 4px 8px; font-size: 11px; }

  /* Player controls minimal */
  .player-controls { gap: 2px; padding: 12px 4px 4px; }
  .custom-vol-bar { width: 30px; }
  .pc-btn svg { width: 16px; height: 16px; }

  /* Toast repositioned */
  .toast-container { top: 48px; right: 4px; left: 4px; }
  .toast { width: 100%; font-size: 11px; padding: 8px 10px; }

  /* Modal wider on small screens */
  .modal { padding: 16px; width: 95vw; max-width: 95vw; }

  .comment-item { padding: 8px 0; gap: 6px; }
  .comment-avatar { width: 24px; height: 24px; font-size: 10px; }
  .comment-author { font-size: 11px; }
  .comment-text { font-size: 11px; }

  /* MEDIA LIST — row/card variants */
  .vlh-idx,.vr-idx,.vlh-stats,.vr-stats{display:none}
  .vr-thumb,.vlh-thumb{width:40px}
  .vr-title{font-size:11px}
  .vr-sub{font-size:9px}
  .clip-grid{grid-template-columns:repeat(auto-fill, minmax(150px, 1fr)); gap:10px;}
  .clip-info{padding:8px 10px}
  .clip-title{font-size:11.5px}
  .clip-meta{flex-direction:column; align-items:flex-start; gap:2px; font-size:9px;}
}

/* ---------- THEATER MODE ---------- */
.app.theater .topbar,
.app.theater .chat-panel,
/* index.html's streamer identity strip. The class name has to match the element the page
   actually renders — this rule is the only thing keeping the strip out of theater mode, and
   nothing fails loudly if it stops matching: the strip simply stays on screen, eating the
   height the player was meant to gain. */
.app.theater .streamer-identity,
/* #streamSection is user.html's counterpart to section.stage: `<main id="channelMain">` IS
   that page's `main` (not a div inside it), so main's direct children there are the profile
   masthead, the tab strip, and the three .channel-section panels — #streamSection is the one
   that has to survive this rule, same relationship section.stage already has on index.html. */
.app.theater main > *:not(.player-wrapper):not(#videoPlayer):not(section.stage):not(#streamSection) { display: none; }

.app.theater main {
  grid-template-columns: 1fr;
}

.app.theater section.stage {
  height: 100vh;
  padding: 0;
}

.app.theater #videoPlayer,
.app.theater .player-wrapper {
  height: 100%;
  aspect-ratio: auto;
}

/* Theater icon toggle */
.pc-btn .theater-icon-collapse { display: none; }
.app.theater .pc-btn .theater-icon-expand { display: none; }
.app.theater .pc-btn .theater-icon-collapse { display: block; }

/* ---------- CHAT PANEL (shared by index.html's #general and the channel page's
   per-streamer room) ---------- */
.chat-panel {border-left:1px solid var(--line); display:flex; flex-direction:column; background:var(--panel); min-height:0}
.c-head{padding:12px; border-bottom:1px solid var(--line); display:flex;justify-content:space-between;font-size:12px;font-weight:bold;flex-shrink:0}
.c-msgs {flex:1; overflow-y:auto; padding:15px; display:flex; flex-direction:column; gap:8px; min-height:0;}
.msg{font-size:13px; line-height:1.4}
.msg-time{color:var(--text-faint); font-size:10px; margin-right:6px; width:36px;display:inline-block}
.msg-user{font-weight:bold; color:var(--text); margin-right:6px; cursor:pointer;}
.msg-text{color:var(--text); word-break:break-word;}
.msg-src-badge{color:var(--discord); border:1px solid var(--discord); border-radius:2px; font-size:9px; font-weight:600; padding:0 3px; margin-right:6px; vertical-align:1px;}
.date-sep{text-align:center;color:var(--text-faint);font-size:10px;padding:8px 0;position:relative;display:flex;align-items:center;gap:10px}
.date-sep::before,.date-sep::after{content:'';flex:1;height:1px;background:var(--line)}

.c-input-wrap{padding:15px; border-top:1px solid var(--line); flex-shrink:0;}
.c-inp-row{display:flex; gap:8px; background:var(--sunken); padding:4px; border:1px solid var(--line);}
.c-inp-row:focus-within {border-color:var(--accent-dim)}
/* color/font-family/font-size don't cascade into form controls the way they do into
   ordinary elements — browsers apply their own UA default font/color to inputs unless a
   rule explicitly overrides it, same reasoning as the "FORM FIELDS (shared)" block above.
   Without this, the chat input silently rendered in each browser's default UI font/color
   (e.g. black Arial) instead of the app's own look, on any page that doesn't happen to
   have some other broad `input{...}` rule already forcing it. */
.c-inp-row input{flex:1; margin:0; background:none; border:none; color:var(--text); font-family:inherit; font-size:inherit; outline:none;}
.send-btn{border:1px solid var(--line); color:var(--text-dim); padding:0 12px; font-size:11px; font-weight:bold;}
.send-btn:hover {color:var(--text); border-color:var(--text);}

/* ---------- COMMENTS (shared by vod-player.html and the channel page's
   VODs/clips subsections) ---------- */
.comments-section {margin-top:24px; border-top:1px solid var(--line); padding-top:20px;}
.comments-header {font-size:14px; font-weight:600; margin-bottom:16px; color:var(--text-dim);}
.comment-form {display:flex; gap:8px; margin-bottom:20px; flex-wrap:wrap;}
.comment-form textarea {flex:1; min-width:200px; padding:8px 12px; resize:vertical; min-height:40px; max-height:150px;}
.comment-form button {align-self:flex-end; white-space:nowrap;}
.comment-item {padding:12px 0; border-bottom:1px solid var(--line); display:flex; gap:10px;}
.comment-avatar {width:28px; height:28px; background:var(--sunken); border:1px solid var(--line); display:flex; align-items:center; justify-content:center; font-weight:bold; color:var(--text-dim); font-size:11px; flex-shrink:0; overflow:hidden;}
.comment-avatar img {width:100%; height:100%; object-fit:cover;}
.comment-body {flex:1; min-width:0;}
.comment-author {font-size:12px; font-weight:600; margin-bottom:2px;}
.comment-text {font-size:12px; color:var(--text); line-height:1.5; word-break:break-word;}
/* markdown-it output inside a comment — kept tight since comments are short, not articles */
.comment-text p {margin:0 0 8px;}
.comment-text p:last-child {margin-bottom:0;}
.comment-text a {color:var(--accent); border-bottom:1px solid transparent;}
.comment-text a:hover {border-bottom-color:var(--accent);}
.comment-text code {background:var(--sunken); border:1px solid var(--line); padding:1px 4px; border-radius:2px; font-size:11px;}
.comment-text pre {background:var(--sunken); border:1px solid var(--line); padding:8px; border-radius:2px; overflow-x:auto; margin:0 0 8px;}
.comment-text pre code {border:none; padding:0; background:none;}
.comment-text ul, .comment-text ol {margin:0 0 8px; padding-left:20px;}
.comment-text blockquote {margin:0 0 8px; padding-left:10px; border-left:2px solid var(--line); color:var(--text-dim);}
.comment-meta {display:flex; gap:8px; align-items:center; margin-top:4px; font-size:10px; color:var(--text-faint);}
.comment-edit-btn, .comment-del-btn {background:none; border:none; cursor:pointer; font-size:10px; padding:2px 4px;}
.comment-edit-btn {color:var(--text-dim);}
.comment-edit-btn:hover {color:var(--accent);}
.comment-del-btn {color:var(--text-faint);}
.comment-del-btn:hover {color:var(--danger);}
.comment-edited {color:var(--text-faint); font-style:italic;}
.comment-textarea {width:100%; padding:6px 10px; resize:vertical; min-height:40px;}

/* ============================================================
   MEDIA LIST (shared) — classes rendered by public/js/media-list.js's row/card/compact
   variants: vods.html's list and grid views, channel.js's media sidebar (below), and
   vod-player.html's related-VODs grid. Used to be four separate, near-duplicate
   implementations (.vr-*, .clip-*, .media-item-*, .related-*) each maintaining their own
   copy of the same thumbnail+duration/type-tag/YouTube-badge/action-menu markup — this is
   the one place both the markup (media-list.js) and its styling (here) live now.

   Shared sub-pieces (.media-tag/.media-badge/.media-avatar/.media-assign/.media-dur) below
   carry only visual identity — color, font, icon sizing — not positioning: a full-width
   table row, a spacious card, and a narrow sidebar item each lay this content out
   differently, so each variant's own selectors (further down) place these pieces rather
   than the shared classes trying to be layout-agnostic on their own. */
.media-tag {font-size:9px; font-weight:700; flex-shrink:0;}
.media-tag.tag-vod {color:var(--accent);}
.media-tag.tag-clip {color:#FBBF24;}

.media-badge {font-size:9px; font-weight:700; flex-shrink:0; text-decoration:none;}
/* No text baseline for an icon-only element to align to inside a baseline-aligned title
   row — browsers fall back to the bottom edge, which reads as "sunk" below the title text.
   align-self overrides that back to simple vertical centering; harmless where this badge
   isn't a flex item at all (the absolutely-positioned thumbnail-overlay contexts below). */
.media-badge.yt {color:var(--youtube); display:inline-flex; align-items:center; align-self:center;}
.media-badge.yt svg {width:13px; height:13px;}
.media-badge.yt:hover {opacity:.8;}
.media-badge.pending {color:#FBBF24;}

.media-avatar {width:20px; height:20px; background:#2A2A2A; border:1px solid var(--line); display:inline-flex; align-items:center; justify-content:center; font-weight:bold; color:#888; font-size:9px; flex-shrink:0; overflow:hidden; vertical-align:middle; margin-right:6px;}
.media-avatar img {width:100%; height:100%; object-fit:cover;}
.media-assign {margin-left:6px; font-size:8px; padding:1px 4px; background:#FBBF24; color:#000; border:1px solid #FBBF24; font-family:inherit; cursor:pointer;}

/* ---- ROW VARIANT — vods.html's list view: compact, old-school row layout (index / thumb
   / title+streamer / date / stats / menu). ---- */
.vod-list {display:flex; flex-direction:column;}
.vod-list-head {display:flex; align-items:center; gap:10px; padding:0 8px 6px; margin-bottom:2px; border-bottom:2px double var(--line); font-size:9px; letter-spacing:.5px; color:var(--text-dim); text-transform:uppercase;}
.vlh-idx {width:24px; flex-shrink:0;}
.vlh-thumb {width:56px; flex-shrink:0;}
.vlh-date {width:80px; flex-shrink:0; text-align:right;}
.vlh-stats {width:130px; flex-shrink:0; text-align:right;}
.vlh-menu {width:20px; flex-shrink:0;}

.vod-row {display:flex; align-items:center; gap:10px; padding:5px 8px; border-bottom:1px solid var(--line-dim); border-left:2px solid transparent; cursor:pointer; transition:background-color .15s, border-color .15s;}
.vod-row:nth-child(even){background:rgba(255,255,255,.02);}
.vod-row:hover{background:var(--panel-2); border-left-color:var(--accent);}

.vr-idx{width:24px; flex-shrink:0; text-align:right; font-size:10px; color:#666; font-variant-numeric:tabular-nums;}
.vr-thumb{position:relative; width:56px; aspect-ratio:16/9; flex-shrink:0; background:var(--sunken); display:flex; align-items:center; justify-content:center; overflow:hidden;}
.vr-thumb img{width:100%; height:100%; object-fit:cover;}
.vr-thumb svg{width:16px; height:16px; opacity:.3;}
.vr-thumb .media-dur{position:absolute; bottom:1px; right:1px; background:rgba(0,0,0,.85); color:#fff; font-size:8px; line-height:1.4; padding:1px 3px; font-variant-numeric:tabular-nums;}

.vr-main{flex:1; min-width:0;}
.vr-title-row{display:flex; align-items:baseline; gap:6px;}
.vr-title{font-size:12px; font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.vr-sub{display:flex; align-items:center; gap:6px; font-size:10px; color:#888; margin-top:1px;}
.vr-sub a{color:inherit;}
.vr-sub a:hover{color:var(--accent);}

.vr-date{width:80px; flex-shrink:0; text-align:right; font-size:10px; color:var(--text-dim);}
.vr-stats{width:130px; flex-shrink:0; text-align:right; font-size:10px; color:var(--text-dim); line-height:1.5;}
.vr-stats span{display:block;}
.vr-actions{flex-shrink:0; width:20px; opacity:0; transition:opacity .15s;}
.vod-row:hover .vr-actions, .vr-actions:has(.open){opacity:1;}
/* Opens downward, unlike the card variant's upward-opening menu below — rows near the top
   of a long list would otherwise get an upward menu clipped by the viewport. */
.vod-row .card-menu-dropdown{top:calc(100% + 4px); min-width:120px; font-size:10px;}
.vod-row .card-menu-dropdown button{padding:6px 10px; font-size:10px;}

/* ---- CARD VARIANT — vods.html's grid view and vod-player.html's related-VODs grid (a
   smaller .related-grid wrapper around the same .clip-card markup, not a second copy of
   it): bigger thumbnails that autoplay the actual VOD/clip (muted, looped) on hover as a
   live preview. */
.clip-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:22px;}
.related-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:12px;}
.clip-card{background:var(--panel); border:1px solid var(--line); cursor:pointer; transition:border-color .2s;}
.clip-card:hover{border-color:#FBBF24;}
.clip-card.is-vod:hover{border-color:var(--accent);}

.clip-thumb{position:relative; width:100%; aspect-ratio:16/9; background:var(--sunken); overflow:hidden;}
.clip-thumb img, .clip-thumb video{position:absolute; inset:0; width:100%; height:100%; object-fit:cover;}
.clip-thumb svg{width:44px; height:44px; opacity:.3; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);}
.clip-thumb video{opacity:0; transition:opacity .25s;}
.clip-thumb.previewing img{opacity:0;}
.clip-thumb.previewing video{opacity:1;}
.clip-thumb .media-dur{position:absolute; bottom:6px; right:6px; background:rgba(0,0,0,.8); color:#fff; font-size:10px; padding:2px 5px; z-index:2; font-variant-numeric:tabular-nums;}
.clip-thumb .media-badge{position:absolute; top:6px; right:6px; padding:2px 6px; z-index:2;}
.clip-thumb .media-badge.yt{background:rgba(0,0,0,.65); display:flex; align-items:center; padding:3px;}
.clip-thumb .media-badge.yt svg{width:16px; height:16px;}
.clip-thumb .media-badge.pending{background:#FBBF24; color:#000;}

/* Static play-icon overlay — a card's thumbnail is otherwise a plain image, with nothing to
   say it's a video. Dims out once the hover preview actually starts playing. The "hover to
   preview" text hint that used to sit beside it is gone (it crowded the corner it shared with
   the YouTube/pending badge); the icon alone is the cue now. */
.media-play{position:absolute; inset:0; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,.15); opacity:.85; transition:opacity .2s; pointer-events:none; z-index:1;}
.clip-thumb.previewing .media-play{opacity:0;}
.media-play svg{width:40px; height:40px; fill:none; stroke:white; stroke-width:1.5;}

.clip-info{padding:12px 14px;}
.clip-title-row{display:flex; align-items:baseline; gap:5px; margin-bottom:6px;}
.clip-title{font-size:13.5px; font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.clip-meta{display:flex; justify-content:space-between; font-size:10.5px; color:#888; margin-bottom:10px;}
.clip-streamer{display:flex; align-items:center; font-size:11.5px; color:#aaa;}
.clip-streamer a{display:inline-flex; align-items:center; color:inherit;}
.clip-streamer a:hover{color:#FBBF24;}
.clip-actions{margin-left:auto; position:relative; flex-shrink:0;}
.clip-card .card-menu-dropdown{bottom:calc(100% + 4px); top:auto; min-width:120px; font-size:10px;}
.clip-card .card-menu-dropdown button{padding:6px 10px; font-size:10px;}

/* Small bordered dots button, all three variants — scoped (rather than a bare .menu-dots-btn
   override) so a page with its own larger dots-button elsewhere (vod-player.html's own
   options menu, outside any of these variants) isn't affected. */
.vod-row .menu-dots-btn, .clip-card .menu-dots-btn, .media-item .menu-dots-btn {border:1px solid transparent; font-size:14px; padding:2px 5px; line-height:1;}
.vod-row .menu-dots-btn:hover, .clip-card .menu-dots-btn:hover, .media-item .menu-dots-btn:hover {border-color:var(--line);}

/* ---- COMPACT VARIANT — channel.js's media sidebar: the scrollable "other VODs"/"other
   clips" list next to a channel page's in-page player — same shell pattern as .chat-panel
   above, just listing media items instead of chat messages. Only used by user.html today,
   but kept here (not page-local) since both its VODs and clips subsections need their own
   independent instance. */
.media-sidebar {border:1px solid var(--line); display:flex; flex-direction:column; background:var(--panel); min-height:0;}
.media-sidebar-head {padding:12px; border-bottom:1px solid var(--line); font-size:12px; font-weight:bold; flex-shrink:0;}
.media-sidebar-list {flex:1; overflow-y:auto; padding:8px; display:flex; flex-direction:column; gap:6px; min-height:0;}
.media-item {display:flex; gap:8px; cursor:pointer; padding:6px; border:1px solid transparent; transition:border-color .2s, background-color .2s;}
.media-item:hover {background:var(--sunken);}
.media-item.active {border-color:var(--accent); background:var(--sunken);}
.media-item-thumb {position:relative; width:108px; flex-shrink:0; aspect-ratio:16/9; background:var(--sunken); overflow:hidden; display:flex; align-items:center; justify-content:center;}
.media-item-thumb img {width:100%; height:100%; object-fit:cover;}
.media-item-thumb svg {opacity:.3; stroke:var(--text-faint);}
.media-item-thumb .media-dur {position:absolute; bottom:3px; right:3px; background:rgba(0,0,0,.8); color:white; font-size:9px; padding:1px 4px;}
.media-item-thumb .media-badge {position:absolute; top:3px; right:3px;}
.media-item-thumb .media-badge.yt {background:rgba(0,0,0,.65); display:flex; align-items:center; padding:2px;}
.media-item-thumb .media-badge.yt svg {width:12px; height:12px;}
.media-item-thumb .media-badge.pending {background:#FBBF24; color:#000; padding:2px 4px;}
.media-item-info {min-width:0; flex:1;}
.media-item-title-row {display:flex; align-items:baseline; gap:4px;}
.media-item-title {font-size:11px; font-weight:600; line-height:1.3; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden;}
.media-item-meta {font-size:9px; color:var(--text-dim); margin-top:4px; display:flex; justify-content:space-between;}
.media-item-actions {flex-shrink:0; align-self:flex-start; opacity:0; transition:opacity .15s;}
.media-item:hover .media-item-actions, .media-item-actions:has(.open) {opacity:1;}
.media-item .card-menu-dropdown {top:calc(100% + 4px); min-width:120px; font-size:10px;}
.media-item .card-menu-dropdown button {padding:6px 10px; font-size:10px;}

/* ---------- TAGS & CATEGORIES (shared) ----------
   Read-only chip row (public/js/media-list.js's renderTagChips) shown under a VOD/clip's
   title on vod-player.html and channel.js's media info panel, plus the editable variant
   inside each page's own "Manage Tags" modal (same .tag-chip base, with a .tag-chip-x
   remove button added). .owner-chip is never backed by a database row — it's rendered
   straight from the vod/clip's own username/avatar fields — hence no remove button ever,
   in either the read-only row or the editable modal list. */
.tag-row {display:flex; flex-wrap:wrap; gap:6px; align-items:center; margin-top:8px;}
.tag-chip {display:inline-flex; align-items:center; gap:5px; padding:3px 9px; font-size:11px; border:1px solid var(--line); border-radius:12px; color:var(--text-dim); background:var(--sunken); line-height:1.4;}
.tag-chip.pending {border-style:dashed; opacity:.65;}
.tag-chip.category-chip {background:none; font-weight:600;}
.tag-chip.owner-chip {border-color:var(--accent); color:var(--text); font-weight:600; padding-left:3px;}
.tag-chip-avatar {width:16px; height:16px; border-radius:50%; background:#2A2A2A; display:flex; align-items:center; justify-content:center; font-size:9px; font-weight:bold; color:#888; overflow:hidden; flex-shrink:0;}
.tag-chip-avatar img {width:100%; height:100%; object-fit:cover;}
.tag-chip-x {background:none; border:none; color:inherit; opacity:.6; cursor:pointer; padding:0; font-size:13px; line-height:1; margin-left:1px;}
.tag-chip-x:hover {opacity:1; color:var(--danger);}
.tag-chip-edit-btn {display:inline-flex; align-items:center; padding:3px 9px; font-size:11px; border:1px dashed var(--line); border-radius:12px; background:none; color:var(--text-dim); cursor:pointer; line-height:1.4;}
.tag-chip-edit-btn:hover {border-color:var(--accent); color:var(--accent);}

/* Manage Tags modal — reuses .modal/.modal-overlay, just wider than the default 400px
   since a category picker + tag input + two chip lists need more room. */
.tags-modal {width:460px;}
.tags-modal h4 {font-size:11px; text-transform:uppercase; color:var(--text-dim); margin:16px 0 6px; font-weight:normal;}
.tags-modal h4:first-of-type {margin-top:0;}
.tags-modal .tag-row {margin-top:4px; min-height:26px;}
.tags-modal-add {display:flex; gap:6px;}
.tags-modal-add select, .tags-modal-add input {flex:1; padding:6px 8px; font-size:12px;}
.tags-modal-add button {padding:6px 12px; font-size:12px; flex-shrink:0;}
.tags-modal-hint {font-size:10px; color:var(--text-faint); margin-top:4px;}

/* ---------- THEME PRESET PICKER (shared) ----------
   Used by both the channel page's own owner-theme editor (user.html) and the Account
   Settings drawer's global theme picker (auth.js, injected on every page) — same button
   look, same THEME_PRESETS data (utils.js), different effect when clicked (stage a preview
   vs. apply-and-save immediately). */
.theme-section-label {font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:.5px; text-align:center; margin-bottom:8px;}
.preset-row {display:flex; flex-wrap:wrap; gap:8px; justify-content:center; margin-bottom:18px;}
.preset-swatch {display:flex; align-items:center; gap:7px; padding:6px 10px; border:1px solid var(--line); background:none; color:var(--text-dim); font-size:11px; font-family:inherit; cursor:pointer; transition:border-color .2s, color .2s;}
.preset-swatch:hover {border-color:var(--accent); color:var(--text);}
.preset-swatch.active {border-color:var(--accent); color:var(--text);}
.preset-dots {display:flex;}
.preset-dot {width:11px; height:11px; border-radius:50%; margin-left:-4px; border:1px solid rgba(0,0,0,.35);}
.preset-dot:first-child {margin-left:0;}
