/* VidReview2 — RDIG house style, translated to a working dark UI.
   Navy base + gold accent (kept sparing: CTAs, active states, timeline
   markers, focus rings — not washed across the interface). Serif headings
   echo the Cambria used in RDIG documents; sans UI echoes Calibri. Both are
   system-font stacks, not a CDN font — this runs behind Loveworld's own
   network and shouldn't depend on reaching an external font host. */

:root {
  --bg: #0B1220;
  --panel: #131C2E;
  --panel-2: #1B2740;
  --border: #29354D;
  --gold: #C9A227;
  --gold-soft: #E4C766;
  --on-accent: #1A1404;
  --text: #EDEAE0;
  --text-muted: #8B96AD;
  --danger: #C4573A;
  --success: #6FA287;

  --font-serif: Iowan Old Style, Palatino Linotype, Georgia, "Noto Serif", serif;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  --radius: 4px;
  --radius-lg: 6px;
}

/* --- Theme presets ---
   Every existing rule already reads colors through these custom
   properties, so a theme is nothing more than reassigning what these
   names point to — no other selector anywhere needs to know a theme
   system exists. --on-accent is the one variable added specifically
   to make this safe: the two light themes below use a DARKER, more
   saturated accent (needed for it to stand out against a light page),
   which means button/badge text on top of it needs to go LIGHT, not
   the dark text that works for the two dark themes' brighter accents.
   Hardcoding one direction for all four would have made one pair or
   the other unreadable. */
[data-theme="midnight-gold"] {
  --bg: #0B1220; --panel: #131C2E; --panel-2: #1B2740; --border: #29354D;
  --gold: #C9A227; --gold-soft: #E4C766; --on-accent: #1A1404;
  --text: #EDEAE0; --text-muted: #8B96AD; --danger: #C4573A; --success: #6FA287;
}
[data-theme="slate-teal"] {
  --bg: #0E1416; --panel: #16201F; --panel-2: #1C2826; --border: #2A3836;
  --gold: #4FB8A6; --gold-soft: #7ED1C2; --on-accent: #072019;
  --text: #E6EDEA; --text-muted: #8FA39D; --danger: #D07A54; --success: #6FA287;
}
[data-theme="ivory-gold"] {
  --bg: #F7F3EA; --panel: #FFFFFF; --panel-2: #F0EAD9; --border: #DDD3BC;
  --gold: #A6791F; --gold-soft: #8C6519; --on-accent: #FFF8E8;
  --text: #2B2418; --text-muted: #6B6252; --danger: #B23A22; --success: #3F7D5C;
}
[data-theme="paper-slate"] {
  --bg: #FFFFFF; --panel: #F7F8FA; --panel-2: #EDEFF3; --border: #D7DBE3;
  --gold: #35507A; --gold-soft: #2A3F60; --on-accent: #F5F7FA;
  --text: #1E2430; --text-muted: #626D80; --danger: #B23A22; --success: #2F7D5C;
}
[data-theme="royal-purple"] {
  --bg: #1A0F2E; --panel: #241640; --panel-2: #2F1D52; --border: #4A3270;
  --gold: #FFFFFF; --gold-soft: #E8E3F5; --on-accent: #1A0F2E;
  --text: #EDE7F6; --text-muted: #A996C4; --danger: #E0654F; --success: #6FA287;
}

* { box-sizing: border-box; }

/* Defensive, not a fix for one specific spot — reported directly from
   a real screenshot: a single unwrapped flex row (share.ejs's header,
   before flex-wrap was added there) was enough to push the WHOLE page
   wider than the viewport, which on a phone silently scrolled
   everything else sideways too — timer labels, hint text, unrelated
   rows nowhere near the actual culprit, all clipped on the left. The
   direct fix for that specific row still matters (this alone doesn't
   stop content from being visually cut off, just stops the outer page
   itself from being draggable/scrollable sideways to reveal it) but
   this is the backstop: whatever the next overflow turns out to be,
   the page itself can't gain horizontal scroll again because of it. */
html, body {
  overflow-x: hidden;
  max-width: 100vw;
}

html, body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 15px;
  line-height: 1.5;
}

/* Hidden by default — the @media (max-width: 500px) block below is
   what actually shows this, on phones only. Without this base rule,
   a browser would render an unstyled, visible default <button> on
   desktop, since the button's only other styling is scoped entirely
   to that mobile media query. */
.floating-add-note-btn { display: none; }

h1, h2, h3 {
  font-family: var(--font-serif);
  font-weight: 500;
  line-height: 1.25;
  margin: 0 0 0.5em;
  color: var(--text);
}
h1 { font-size: 1.75rem; }
h2 { font-size: 1.3rem; }
h3 { font-size: 1.05rem; }

p { margin: 0 0 1em; color: var(--text-muted); max-width: 68ch; }

a { color: var(--gold-soft); text-decoration: none; }
a:hover { text-decoration: underline; }

button, input, select, textarea { font-family: inherit; font-size: inherit; color: inherit; }

:focus-visible {
  outline: 2px solid var(--gold);
  outline-offset: 2px;
}

/* --- App shell --- */

.app-shell {
  display: flex;
  min-height: 100vh;
}
/* Only review.ejs opts into this — the fixed height + clipped overflow
   is what makes the two-column desktop layout work (video area and
   notes panel each scroll independently within their own column,
   neither one able to grow the outer page). This used to be an inline
   style directly in review.ejs's markup, which is exactly why it kept
   surviving every previous mobile fix in this file: an inline style
   overrides any external stylesheet rule regardless of specificity or
   media query, so nothing in this file could ever have touched it
   without being moved into a class first. */
.review-app-shell { height: 100vh; overflow: hidden; }

.sidebar {
  width: 220px;
  flex-shrink: 0;
  background: var(--panel);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  padding: 20px 0;
}

.sidebar-brand {
  padding: 0 20px 20px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 12px;
  display: flex;
  align-items: center;
}
.sidebar-brand .name { font-family: var(--font-serif); font-size: 1.15rem; }
.sidebar-brand .tag { font-size: 0.72rem; color: var(--text-muted); letter-spacing: 0.03em; }

/* Grammar Rules tab in the admin panel (views/admin.ejs). */
.grammar-admin-input { padding: 8px 10px; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font: inherit; box-sizing: border-box; }
.grammar-admin-input:focus { outline: 2px solid var(--gold); outline-offset: 1px; }
.grammar-form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 10px; }
.grammar-form-grid label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; color: var(--text-muted); }
.grammar-form-grid .grammar-admin-input { width: 100%; }
.grammar-rule-off { opacity: 0.55; }
.grammar-wrong { overflow-wrap: anywhere; }

/* Grammar check panel on the review page (views/review.ejs). */
.grammar-context { line-height: 1.55; overflow-wrap: anywhere; }
.grammar-mark { background: rgba(201, 162, 39, 0.3); color: inherit; border-radius: 3px; padding: 0 2px; }
.grammar-dismissed { opacity: 0.65; }
.grammar-filters { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }

/* "Pick up where you left off?" above the player (public/js/resume-playback.js). It sits
   in the page flow ABOVE the video frame — not over it, where it would fight the drawing
   canvas — and uses the theme variables so it follows every colour theme. */
.resume-prompt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  padding: 10px 14px;
  margin-bottom: 12px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-left: 3px solid var(--gold);
  border-radius: var(--radius);
  font-size: 0.9rem;
}
.resume-prompt-text { flex: 1 1 12rem; min-width: 0; }
.resume-prompt-dismiss {
  background: none;
  border: 0;
  color: var(--text-muted);
  font-size: 1.3rem;
  line-height: 1;
  padding: 2px 6px;
  cursor: pointer;
}
.resume-prompt-dismiss:hover { color: var(--text); }

/* The logo and app name as a link (partials/brand-link-attrs.ejs). AT REST it must
   look exactly as it did before — same position, size, colour, font, no underline —
   which was measured, pixel for pixel, on every page. On hover it picks up the app's
   ordinary link hover (an underline, via the global a:hover rule) on purpose: that is
   what tells a share-page visitor the name is now clickable. */
.brand-link { color: inherit; text-decoration: none; }
.brand-link:hover { opacity: 0.88; }
.brand-link:focus-visible { outline: 2px solid var(--gold); outline-offset: 3px; border-radius: 4px; }
.sidebar-brand .brand-link { display: flex; align-items: center; min-width: 0; }
.landing-hero .brand-link { display: block; }

.nav-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 14px 10px;
  margin-bottom: 10px;
  color: var(--text-muted);
  font-size: 0.78rem;
  text-align: center;
  border-left: 2px solid transparent;
}
.nav-item:hover { color: var(--text); text-decoration: none; background: var(--panel-2); }
.nav-item.active { color: var(--gold-soft); border-left-color: var(--gold); background: var(--panel-2); }
.nav-icon {
  display: inline-flex;
  justify-content: center;
  width: auto;
  font-size: 1.9rem;
  line-height: 1;
  opacity: 0.9;
}
.sidebar-menu-toggle { display: none; }

.main {
  flex: 1;
  min-width: 0;
  padding: 32px 40px;
}

.main-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 24px;
  gap: 16px;
  flex-wrap: wrap;
}
.user-menu { position: relative; align-self: center; }
/* Groups the notification bell and the account menu as a single flex
   unit — matters because some pages include this partial directly as
   a flex child of a space-between header row (feedback.ejs,
   upload.ejs) rather than wrapping it in their own row div
   (dashboard.ejs does). Without this wrapper, the bell and the avatar
   were two separate flex items and space-between spaced them far
   apart from each other instead of keeping them grouped together. */
.header-icon-group { display: flex; align-items: center; gap: 8px; }
.notif-bell-wrap { position: relative; align-self: center; margin-right: 0; }
.notif-bell-trigger {
  /* Matches .theme-picker-trigger's contained-button look — a bare
     emoji with no background/border reads as a stray, misplaced
     glyph next to a properly bounded button like the theme picker's. */
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 10px;
  cursor: pointer;
  font-size: 0.95rem;
  line-height: 1;
  position: relative;
}
.notif-bell-badge {
  position: absolute; top: -4px; right: -4px;
  background: var(--danger, #E85D4A); color: #fff;
  font-size: 0.62rem; font-weight: 700; line-height: 1;
  min-width: 15px; height: 15px; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  padding: 0 3px;
}
.notif-bell-dropdown {
  position: absolute; top: calc(100% + 8px); right: 0;
  background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius);
  width: 340px; max-height: 420px; overflow-y: auto; overflow-x: hidden; z-index: 50;
  box-shadow: 0 8px 24px rgba(0,0,0,0.3);
}
.notif-bell-header {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 12px; border-bottom: 1px solid var(--border); font-weight: 600; font-size: 0.85rem;
}
.notif-bell-item {
  display: block; padding: 10px 12px; border-bottom: 1px solid var(--border);
  text-decoration: none; cursor: pointer;
}
.notif-bell-item:last-child { border-bottom: none; }
.notif-bell-item:hover { background: var(--panel-2); }
.notif-bell-item.unread { background: rgba(212, 175, 55, 0.06); }
.notif-bell-title {
  font-weight: 600; font-size: 0.85rem; color: var(--text);
  display: flex; align-items: center;
}
.notif-bell-item.unread .notif-bell-title::before { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--gold); margin-right: 6px; flex-shrink: 0; }
.notif-bell-preview {
  color: var(--text-muted); font-size: 0.8rem; margin-top: 2px;
  /* Clamped to 2 lines rather than left to run on — this is the raw
     KingsChat message text, written as a full sentence for a chat
     bubble, not pre-sized for a compact inbox row. overflow-wrap as a
     second line of defense so even one very long unbroken word (a
     URL, say) can't force the row wider than the dropdown itself. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  overflow-wrap: break-word;
}
.notif-bell-time { color: var(--text-muted); font-size: 0.72rem; margin-top: 4px; }
/* A web address inside a message (public/js/linkify.js). Always underlined so
   it reads as a link even inside muted, clickable text; long addresses wrap
   rather than widen the row. */
.mvr-link { text-decoration: underline; overflow-wrap: anywhere; }
/* A message that CONTAINS a link shows in full. The 2-line clamp above would
   cut it off — almost every message ends with its link, after a sentence
   that is already two lines long — and a link you can't see is not clickable
   in any way that matters. Messages without a link keep the compact clamp. */
.notif-bell-preview.has-link { display: block; -webkit-line-clamp: unset; overflow: visible; overflow-wrap: anywhere; }
.user-menu-trigger {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  display: block;
  line-height: 0;
}
.user-avatar {
  width: 34px; height: 34px;
  border-radius: 50%;
  object-fit: cover;
  border: 1px solid var(--border);
}
.user-avatar-fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel-2);
  color: var(--gold);
  font-weight: 600;
  font-size: 0.85rem;
}
.user-menu-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  min-width: 180px;
  z-index: 50;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.user-menu-name { font-weight: 600; font-size: 0.88rem; }

.theme-picker { position: relative; align-self: center; }
.theme-picker-trigger {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 6px 10px;
  cursor: pointer;
  color: var(--text);
  font-size: 0.95rem;
  line-height: 1;
}
.theme-picker-dropdown {
  position: absolute;
  top: calc(100% + 8px);
  right: 0;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px;
  min-width: 190px;
  z-index: 50;
  box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.theme-picker-label { font-weight: 600; font-size: 0.82rem; color: var(--text-muted); margin-bottom: 8px; }
.theme-swatch-row { display: flex; gap: 10px; }
.theme-swatch {
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 2px solid var(--border);
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.theme-swatch.active { border-color: var(--text); }
.theme-swatch span { display: block; width: 14px; height: 14px; border-radius: 50%; }

/* --- Interactive tour guide ---
   Spotlight effect via z-index, not a clip-path cutout: the darkened
   overlay covers the whole page, and the highlighted element is simply
   raised above it with its own z-index plus a glow — same well-
   established, cross-browser-safe technique used by most simple tour
   libraries, and far less risky than clip-path/mask math given this
   app's history with CSS layout bugs. */
.tour-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.65);
  z-index: 9000;
}
.tour-highlight {
  position: relative;
  z-index: 9001;
  box-shadow: 0 0 0 4px var(--gold), 0 0 30px rgba(0,0,0,0.6);
  border-radius: var(--radius);
}
.tour-tooltip {
  position: fixed;
  z-index: 9002;
  background: var(--panel);
  border: 1px solid var(--gold);
  border-radius: var(--radius-lg);
  padding: 16px;
  width: 320px;
  max-width: calc(100vw - 24px);
  max-height: calc(100vh - 24px);
  overflow-y: auto;
  box-shadow: 0 10px 40px rgba(0,0,0,0.5);
}
.tour-tooltip h4 { margin: 0 0 8px; font-family: var(--font-serif); font-size: 1.1rem; overflow-wrap: break-word; }
.tour-tooltip p { margin: 0 0 14px; font-size: 0.88rem; color: var(--text); line-height: 1.5; overflow-wrap: break-word; }
.tour-progress { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 8px; }
.tour-dont-show-again {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: 14px;
  cursor: pointer;
}
.tour-dont-show-again input { margin: 0; flex-shrink: 0; }
.tour-buttons { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 8px; }
.tour-start-btn { position: fixed; bottom: 20px; right: 20px; z-index: 8000; }

/* --- Announcements: platform-wide notices, see routes/admin.js and
   partials/announcements.ejs. Overlay, not reflow — deliberately for
   this first version, since several pages use a fixed 100vh shell
   with their own internal scrolling, and safely pushing their content
   down/up would need per-page changes rather than one shared partial.
   Multiple simultaneous announcements of the same style stack via
   these flex containers rather than overlapping. */
.announcement-bar-container {
  position: fixed;
  left: 0;
  right: 0;
  z-index: 7000;
  display: flex;
  flex-direction: column;
  pointer-events: none;
}
.announcement-bar-container-top { top: 0; }
.announcement-bar-container-bottom { bottom: 0; flex-direction: column-reverse; }
.announcement-bar {
  position: relative;
  background: var(--gold);
  color: var(--on-accent);
  font-weight: 600;
  font-size: 0.88rem;
  text-align: center;
  padding: 10px 40px;
  pointer-events: auto;
}
.announcement-popup-container {
  position: fixed;
  bottom: 20px;
  left: 20px;
  z-index: 7000;
  display: flex;
  flex-direction: column-reverse;
  gap: 10px;
  max-width: 340px;
  pointer-events: none;
}
.announcement-popup {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--gold);
  border-radius: var(--radius-lg);
  padding: 14px 36px 14px 16px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.45);
  font-size: 0.88rem;
  color: var(--text);
  pointer-events: auto;
}
.announcement-dismiss-btn {
  position: absolute;
  top: 50%;
  right: 10px;
  transform: translateY(-50%);
  background: transparent;
  border: none;
  color: inherit;
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  padding: 4px 8px;
  opacity: 0.75;
}
.announcement-dismiss-btn:hover { opacity: 1; }
@media (max-width: 900px) {
  .announcement-popup-container { left: 12px; right: 12px; bottom: 12px; max-width: none; }
}

/* PWA install banner — deliberately prominent (a floating card with its
   own icon and headline), not a small icon tucked into a corner. Sits
   above the tour's own floating button but below an active tour's
   overlay, so a person mid-tour isn't interrupted by this appearing on
   top of it. */
.pwa-install-banner {
  position: fixed;
  left: 16px;
  right: 16px;
  bottom: 16px;
  max-width: 440px;
  margin: 0 auto;
  background: var(--panel);
  border: 1px solid var(--gold);
  border-radius: var(--radius-lg);
  padding: 16px;
  box-shadow: 0 12px 40px rgba(0,0,0,0.45);
  z-index: 8500;
  display: flex;
  align-items: center;
  gap: 14px;
}
.pwa-install-banner img { width: 52px; height: 52px; border-radius: 12px; flex-shrink: 0; }
.pwa-install-banner .pwa-banner-text { flex: 1; min-width: 0; }
.pwa-install-banner .pwa-banner-title { font-weight: 600; margin-bottom: 2px; }
.pwa-install-banner .pwa-banner-sub { font-size: 0.8rem; color: var(--text-muted); }
.pwa-install-banner .pwa-banner-actions { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }

@media (max-width: 500px) {
  .pwa-install-banner { flex-wrap: wrap; }
  .pwa-install-banner .pwa-banner-actions { flex-direction: row; width: 100%; }
  .pwa-install-banner .pwa-banner-actions button { flex: 1; }
  /* Keyboard shortcuts don't apply on a touchscreen — no physical
     keyboard to press Space/J/L/K/N on. Reported directly as mobile
     clutter, not something a phone visitor can act on anyway. */
  #keyboardHint { display: none; }

  /* Mobile only — on desktop the compose box is already a short
     scroll below the video; on a phone, with the scrubber, controls,
     and version/compare panels all stacked above it, it can be a long
     way down. env(safe-area-inset-*) keeps this clear of the home
     indicator/gesture bar on notched phones — this app's own meta
     viewport tag already declares viewport-fit=cover, so these
     resolve to real values, not always zero.

     Reported directly: this button sat at the exact same bottom-right
     20px spot as the Tour button (.tour-start-btn, always present,
     not just on this page), which has a far higher z-index — so this
     one was rendering fully hidden behind it, not just visually
     close. Stacked comfortably above it instead: the Tour button is a
     small .btn-sm (roughly 30-36px tall including padding/border) at
     bottom:20px, so 88px of clearance here leaves a real gap rather
     than a tight, easy-to-misjudge fit. */
  .floating-add-note-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    right: calc(20px + env(safe-area-inset-right, 0px));
    bottom: calc(88px + env(safe-area-inset-bottom, 0px));
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--gold);
    color: var(--on-accent);
    border: none;
    font-size: 1.8rem;
    line-height: 1;
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
    cursor: pointer;
    z-index: 40;
  }
  .floating-add-note-btn:active { transform: scale(0.94); }
}

/* KingsChat login button — deliberately hardcoded colors, not theme
   variables, so this always looks the same (KingsChat's own sky blue)
   regardless of which of the five themes is active. Everything else in
   this app follows the theme; this one button is a brand mark and
   shouldn't. */
.btn-kingschat {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  background: #00AEEF;
  color: #FFFFFF;
  border: 1px solid #00AEEF;
  border-radius: var(--radius);
  padding: 12px 20px;
  font-weight: 600;
  font-size: 0.95rem;
  cursor: pointer;
  text-decoration: none;
  transition: background 0.15s, border-color 0.15s;
}
.btn-kingschat:hover { background: #0090C7; border-color: #0090C7; text-decoration: none; color: #FFFFFF; }
.btn-kingschat svg { flex-shrink: 0; }

/* --- Landing page --- */
.landing-hero {
  min-height: 78vh;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 60px 24px 100px;
  background-size: cover;
  background-position: center;
  background-color: var(--bg);
  position: relative;
}
/* Dark scrim over whatever background image is set, so the headline
   stays readable regardless of what image an admin uploads — without
   this, a bright or busy photo could make the text illegible and
   there'd be no way to know that in advance. */
.landing-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(0,0,0,0.55), rgba(0,0,0,0.75));
}
.landing-hero-content { position: relative; z-index: 1; max-width: 640px; }
.landing-logo { display: flex; justify-content: center; margin-bottom: 20px; }
.landing-logo img { max-height: 64px; max-width: 220px; object-fit: contain; }
.landing-logo-placeholder {
  width: 64px; height: 64px;
  border-radius: 50%;
  border: 2px dashed rgba(255,255,255,0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.8rem;
  background: rgba(255,255,255,0.06);
}
.landing-brand { font-family: var(--font-serif); font-size: 1.3rem; color: #FFFFFF; margin-bottom: 24px; }
.landing-hero h1 { font-size: clamp(2rem, 5vw, 3.2rem); color: #FFFFFF; margin: 0 0 16px; line-height: 1.15; }
.landing-hero p.landing-subheadline { font-size: 1.1rem; color: rgba(255,255,255,0.85); margin: 0 0 32px; line-height: 1.5; }
.landing-guest-note { font-size: 0.82rem; color: rgba(255,255,255,0.65); margin-top: 18px; }

.landing-hero-image-wrap { display: flex; justify-content: center; margin-top: -70px; padding: 0 24px; position: relative; z-index: 2; }
.landing-hero-image {
  width: 100%;
  max-width: 900px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  box-shadow: 0 20px 60px rgba(0,0,0,0.4);
  border: 1px solid var(--border);
}
.landing-hero-image img { width: 100%; display: block; }
.landing-hero-placeholder {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: var(--panel);
  border: 2px dashed var(--border);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: var(--text-muted);
  font-size: 0.85rem;
  padding: 20px;
  text-align: center;
}
.landing-hero-placeholder .icon { font-size: 2.5rem; opacity: 0.5; }

.landing-features {
  max-width: 1000px;
  margin: 80px auto 60px;
  padding: 0 24px;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 32px;
}
.landing-feature h3 { font-family: var(--font-serif); font-size: 1.2rem; margin: 0 0 10px; }
.landing-feature p { color: var(--text-muted); font-size: 0.92rem; line-height: 1.6; margin: 0; }

@media (max-width: 800px) {
  .landing-features { grid-template-columns: 1fr; gap: 28px; }
  .landing-hero-image-wrap { margin-top: -40px; }
}

/* --- Buttons --- */

.btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
  font-size: 0.88rem;
  transition: border-color 120ms ease;
}
.btn:hover { border-color: var(--text-muted); text-decoration: none; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }

.btn-primary {
  background: var(--gold);
  border-color: var(--gold);
  color: var(--on-accent);
  font-weight: 600;
}
.btn-primary:hover { background: var(--gold-soft); border-color: var(--gold-soft); }

.btn-danger { border-color: var(--danger); color: var(--danger); background: transparent; }
.btn-danger:hover { background: var(--danger); color: var(--bg); }

.btn-success { background: var(--success); border-color: var(--success); color: var(--on-accent); font-weight: 600; }
.btn-success:hover { opacity: 0.88; }

.btn-success-outline { border-color: var(--success); color: var(--success); background: transparent; }
.btn-success-outline:hover { background: var(--success); color: var(--bg); }

/* Deliberately vivid, saturated colors rather than the theme's own
   (much more muted) --success/--danger — these two exist specifically
   to visually shout "look here next," which a subtle theme-matched
   tone would undercut. Green flashes slower (2.4s) as a gentle nudge
   toward a ready action; red flashes faster (1s) since a blocked
   state is the more urgent of the two. */
@keyframes flash-green-cta {
  0%, 100% { background: #16a34a; box-shadow: 0 0 0 rgba(34, 197, 94, 0); }
  50% { background: #22c55e; box-shadow: 0 0 14px rgba(34, 197, 94, 0.65); }
}
.btn-flash-green {
  animation: flash-green-cta 2.4s ease-in-out infinite;
  border-color: transparent;
  color: #ffffff;
  font-weight: 600;
}
@keyframes flash-red-cta {
  0%, 100% { background: #b91c1c; box-shadow: 0 0 0 rgba(239, 68, 68, 0); }
  50% { background: #ef4444; box-shadow: 0 0 14px rgba(239, 68, 68, 0.65); }
}
.btn-flash-red {
  animation: flash-red-cta 1s ease-in-out infinite;
  border-color: transparent;
  color: #ffffff;
  font-weight: 600;
}
@media (prefers-reduced-motion: reduce) {
  .btn-flash-green, .btn-flash-red { animation: none; }
}

.btn-sky { background: #0ea5e9; border-color: #0ea5e9; color: #ffffff; font-weight: 600; }
.btn-sky:hover { background: #0284c7; border-color: #0284c7; }

@keyframes flash-yellow-cta {
  0%, 100% { background: #ca8a04; box-shadow: 0 0 0 rgba(250, 204, 21, 0); }
  50% { background: #facc15; box-shadow: 0 0 16px rgba(250, 204, 21, 0.7); }
}
.btn-flash-yellow {
  animation: flash-yellow-cta 2s ease-in-out infinite;
  border-color: transparent;
  color: #1A1404;
  font-weight: 700;
  padding: 5px 10px;
  font-size: 0.8rem;
}
@media (prefers-reduced-motion: reduce) {
  .btn-flash-yellow { animation: none; }
}

.btn-sm { padding: 5px 10px; font-size: 0.8rem; }

/* --- Stats row: pairs compact usage panels (storage, transcription)
   side by side on wider screens, stacking on narrow ones — same
   auto-fit pattern as .quick-actions below, just a wider minimum
   since these panels hold more content per card. --- */
.stats-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 20px;
  margin-bottom: 24px;
}

/* --- Panels / ledger list (flat, bordered — no drop shadows) --- */

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 20px;
}

/* Requested directly: tidy up the admin panel for desktop — make
   better use of the available width instead of a single narrow
   column of stacked cards, so settings panels sit side by side and
   there's less page-length scrolling to get through them all. Scoped
   to desktop widths only; narrower viewports keep the original
   single-column stack, which is what they need.
   .tab-panel is the direct container for each admin section's cards
   (see admin.ejs) — turning it into a grid lets its .panel children
   flow into columns automatically, sized to at least 420px so a
   small settings card never gets awkwardly squeezed. Wide content
   (tables, record lists, long forms) opts out with .panel-full so it
   still spans the row it needs, rather than being forced into a
   column meant for a compact settings card. */
@media (min-width: 1200px) {
  .tab-panel {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: 20px;
    align-items: start;
  }
  .panel-full { grid-column: 1 / -1; }
}

/* Requested directly, alongside the above: reduce how much most
   record lists make the whole page scroll — a long list now scrolls
   within its own bounded box instead of pushing everything below it
   further down the page. Applied to the specific list container
   inside a panel (not the panel itself), so the panel's heading and
   any controls above the list stay visible while the list scrolls.
   Desktop-only, same reasoning as the grid above — a short mobile
   viewport needs the page's own scroll, not another nested one. */
@media (min-width: 1200px) {
  .scroll-list { max-height: 65vh; overflow-y: auto; }
}

.quick-actions {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: 12px;
  margin-bottom: 32px;
}
.quick-action {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: 18px;
  text-align: left;
  color: var(--text);
}
.quick-action:hover { border-color: var(--gold); text-decoration: none; }
.quick-action .label { font-family: var(--font-serif); font-size: 1rem; display: block; margin-top: 8px; }
.quick-action .icon { color: var(--gold); font-size: 1.3rem; }

/* Purposeful color, not decoration: gold marks the primary/most-common
   action, success marks a positive "create/start something" action —
   consistent with how those two colors are already used everywhere
   else (btn-primary, btn-success). */
.quick-action.accent-gold { border-top: 3px solid var(--gold); }
.quick-action.accent-success { border-top: 3px solid var(--success); }
.quick-action.accent-success .icon { color: var(--success); }
.quick-action.accent-success:hover { border-color: var(--success); }

.ledger {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.ledger-row {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
}
.ledger-row:last-child { border-bottom: none; }
.ledger-row:hover { background: var(--panel-2); text-decoration: none; }
.ledger-thumb {
  width: 128px; height: 72px;
  object-fit: cover;
  border-radius: 4px;
  background: var(--panel-2);
  flex-shrink: 0;
}
.ledger-title { font-weight: 600; font-size: 0.92rem; }
.ledger-meta { font-size: 0.78rem; color: var(--text-muted); }
.ledger-row .spacer { flex: 1; }

/* --- Grid view (YouTube-style cards) --- */

.view-toggle {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.view-toggle button {
  padding: 6px 12px;
  background: var(--panel-2);
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 0.82rem;
}
.view-toggle button.active { background: var(--gold); color: var(--on-accent); font-weight: 600; }
.view-toggle button:not(.active):hover { color: var(--text); }

.video-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: 20px;
}
.video-card {
  display: block;
  color: var(--text);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.video-card:hover { text-decoration: none; }
.video-card-thumb-wrap {
  position: relative;
  aspect-ratio: 16 / 9;
  background: var(--panel-2);
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: 10px;
}
.video-card-thumb-wrap img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}
.video-card:hover .video-card-thumb-wrap { outline: 2px solid var(--gold); outline-offset: 2px; }
/* Sits on top of the static <img>, hidden until a sprite sheet is
   both available and actively being scrubbed — background-position is
   swapped on mousemove rather than swapping an <img src>, so "seeking"
   is instant with no flicker and no per-frame network request. */
.video-card-scrub {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 100ms ease;
  pointer-events: none;
}
.video-card-scrub.active { opacity: 1; }
.video-card-duration {
  position: absolute;
  bottom: 6px; right: 6px;
  background: rgba(11, 18, 32, 0.85);
  color: var(--text);
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  padding: 2px 6px;
  border-radius: 3px;
}
.video-card-title {
  font-weight: 600;
  font-size: 0.92rem;
  margin-bottom: 4px;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.video-card-meta { font-size: 0.78rem; color: var(--text-muted); }
.video-card-actions { margin-top: 8px; }

.empty-state {
  padding: 40px 20px;
  text-align: center;
  color: var(--text-muted);
  border: 1px dashed var(--border);
  border-radius: var(--radius-lg);
}

/* --- Badges --- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.75rem;
  color: var(--text-muted);
}
.badge::before {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--text-muted);
}
.badge.in_review::before { background: var(--gold); }
.badge.in_review { color: var(--gold-soft); }
.badge.approved::before { background: var(--success); }
.badge.approved { color: var(--success); }
.badge.pending_approval::before { background: #6C9BD1; }
.badge.pending_approval { color: #6C9BD1; }
.badge.archived::before { background: var(--text-muted); }
.badge.danger::before { background: var(--danger); }
.badge.danger { color: var(--danger); }

.tab-btn { background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding-bottom: 10px; color: var(--text-muted); }
.tab-btn:hover { color: var(--text); background: transparent; }
.tab-btn.active { color: var(--text); border-bottom-color: var(--gold); font-weight: 600; }

/* --- Forms --- */

.field { margin-bottom: 16px; }
.field label { display: block; font-size: 0.82rem; color: var(--text-muted); margin-bottom: 6px; }
.field input[type=text], .field input[type=file], .field textarea, .field select {
  width: 100%;
  padding: 9px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
}
.field input:focus, .field textarea:focus, .field select:focus { border-color: var(--gold); }
.field .hint { font-size: 0.76rem; color: var(--text-muted); margin-top: 4px; }
.field .error { font-size: 0.8rem; color: var(--danger); margin-top: 6px; }

/* --- Review player screen --- */

.review-layout {
  display: grid;
  grid-template-columns: 1fr 360px;
  gap: 0;
  height: calc(100vh - 0px);
}
/* share.ejs specifically wants a wider notes column than review.ejs's
   default 360px — a compound selector (both classes) rather than a
   separate .share-review-layout rule alone, so this has higher
   specificity than the base rule above and doesn't depend on source
   order to win. The matching mobile override below uses the same
   compound selector for the same reason — an equally-specific rule
   inside the media query, not a lower-specificity one that this
   desktop rule would otherwise defeat regardless of viewport width. */
.review-layout.share-review-layout {
  grid-template-columns: 1fr 460px;
}
.review-main {
  padding: 24px 32px;
  overflow-y: auto;
}
.review-video-frame {
  background: #000;
  border-radius: var(--radius-lg);
  overflow: hidden;
  margin-bottom: 12px;
  position: relative;
}
.review-video-frame video { width: 100%; display: block; max-height: 62vh; }

#drawingCanvas {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  pointer-events: none; /* only interactive while actively drawing — see JS */
  touch-action: none; /* prevents scroll/zoom gestures from hijacking a finger-drawn stroke */
}
#drawingCanvas.drawing-active { pointer-events: auto; cursor: crosshair; }
#drawingToolbar {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  margin-top: 10px;
}
.drawColorBtn {
  width: 22px; height: 22px;
  border-radius: 50%;
  border: 2px solid transparent;
  cursor: pointer;
  padding: 0;
}
.drawColorBtn.active { border-color: var(--text); }

.scrubber {
  position: relative;
  height: 40px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-bottom: 16px;
}
.scrubber-track {
  position: absolute;
  top: 50%; left: 12px; right: 12px;
  height: 3px;
  background: var(--border);
  transform: translateY(-50%);
  border-radius: 2px;
}
.scrubber-fill {
  position: absolute; top: 0; left: 0; bottom: 0;
  background: var(--gold);
  opacity: 0.25;
  border-radius: 2px;
}
.scrubber-playhead {
  position: absolute;
  top: 50%;
  width: 12px; height: 12px;
  background: var(--gold);
  border: 2px solid var(--bg);
  border-radius: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  pointer-events: none;
}
@keyframes playhead-radar-ping {
  0% { box-shadow: 0 0 0 0 rgba(201, 162, 39, 0.7); }
  70% { box-shadow: 0 0 0 26px rgba(201, 162, 39, 0); }
  100% { box-shadow: 0 0 0 26px rgba(201, 162, 39, 0); }
}
.scrubber-playhead.radar-jump { animation: playhead-radar-ping 900ms ease-out 2; }
@media (prefers-reduced-motion: reduce) {
  .scrubber-playhead.radar-jump { animation: none; }
}
.scrubber-marker {
  position: absolute;
  top: 50%;
  width: 10px; height: 10px;
  background: var(--gold);
  transform: translate(-50%, -50%) rotate(45deg);
  cursor: pointer;
  border: 1px solid var(--bg);
}
.scrubber-marker.resolved { background: var(--success); opacity: 0.6; }
.scrubber-marker:hover { background: var(--gold-soft); }
@keyframes marker-pulse {
  0% { box-shadow: 0 0 0 0 rgba(201, 162, 39, 0.6); }
  100% { box-shadow: 0 0 0 10px rgba(201, 162, 39, 0); }
}
.scrubber-marker.new { animation: marker-pulse 900ms ease-out 1; }

/* Copyright matches are a RANGE (start to end), not a single instant —
   a diamond point marker doesn't represent that correctly, so this is
   a spanning bar across the flagged duration instead. Sits below the
   diamond markers in the same track (z-index lower), so a note marker
   landing within a flagged range is never visually hidden behind it. */
.scrubber-copyright-marker {
  position: absolute;
  top: 50%;
  height: 8px;
  background: #E85D4A;
  opacity: 0.55;
  transform: translateY(-50%);
  border-radius: 2px;
  cursor: pointer;
  z-index: 1;
}
.scrubber-copyright-marker:hover { opacity: 0.8; }

.player-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 8px;
  gap: 10px;
  margin-bottom: 24px;
  font-variant-numeric: tabular-nums;
  font-size: 0.85rem;
  color: var(--text-muted);
}

.comment-panel {
  background: var(--panel);
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  height: 100vh;
}
/* Hidden on desktop by default — only shown and made functional inside
   the mobile media query below, where Notes and Captions are long
   enough sections each that showing both fully expanded at once makes
   the page unwieldy to scroll through. No base rule for
   mobile-tab-hidden here at all — an earlier version set it to
   `display: block`, meant as a harmless desktop no-op, but that
   collided with .btn's `display: inline-flex` on the same element
   (#toggleCaptionsBtn carries both classes): equal specificity, tied
   broken by source order, and the later rule silently won — forcing a
   real button into a full-width block layout on desktop, which is
   very likely what read as "the panel overlapping." The only safe
   no-op is no rule at all, so the element's own classes decide its
   display type outside the one context (mobile) where this class is
   meant to do anything. */
.mobile-panel-tabs { display: none; }
.comment-panel-header {
  padding: 20px 20px 14px;
  border-bottom: 1px solid var(--border);
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
}
.comment-list {
  flex: 1;
  overflow-y: auto;
  padding: 12px 16px;
}
.comment {
  padding: 12px 0;
  border-bottom: 1px solid var(--border);
}
.comment:last-child { border-bottom: none; }
.comment-meta {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 4px;
  font-size: 0.8rem;
}
.comment-author { font-weight: 600; color: var(--text); }
.comment-time { color: var(--gold-soft); cursor: pointer; }
.comment-time:hover { text-decoration: underline; }
.comment-time-of-day { color: var(--text-muted); font-size: 0.7rem; margin-left: 2px; }
.comment-readonly-banner {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 10px;
  font-size: 0.8rem;
  color: var(--text-muted);
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
}
.todo-item {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 10px 20px;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  font-size: 0.85rem;
}
.todo-item:hover { background: var(--panel-2); }
.todo-item .todoCheckbox { margin-top: 3px; cursor: pointer; }
.todo-text { color: var(--text); }
#todoToggleBtn.active { background: var(--gold); color: var(--on-accent); border-color: var(--gold); }
.comment-body { font-size: 0.88rem; color: var(--text); margin: 0; }
.comment-guest-tag {
  font-size: 0.68rem;
  color: var(--text-muted);
  border: 1px solid var(--border);
  border-radius: 3px;
  padding: 1px 5px;
}
.comment-thread { margin-left: 16px; margin-top: 8px; border-left: 2px solid var(--border); padding-left: 12px; }

.comment-compose {
  padding: 14px 16px;
  border-top: 1px solid var(--border);
}
.comment-compose textarea {
  width: 100%;
  min-height: 60px;
  padding: 10px 12px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  resize: vertical;
  margin-bottom: 8px;
}
.comment-compose-row { display: flex; justify-content: space-between; align-items: center; }
.compose-timecode { font-size: 0.78rem; color: var(--text-muted); font-variant-numeric: tabular-nums; }

/* --- Role builder matrix --- */

.role-matrix { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.role-matrix th, .role-matrix td {
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  text-align: left;
}
.role-matrix th { color: var(--text-muted); font-weight: 500; font-size: 0.78rem; }
.role-matrix td.perm-name { color: var(--text-muted); }
.role-matrix input[type=checkbox] { accent-color: var(--gold); width: 16px; height: 16px; }

/* --- Utility --- */

.stack-sm > * + * { margin-top: 8px; }
.row { display: flex; align-items: center; gap: 10px; }
.text-muted { color: var(--text-muted); }
.mt-24 { margin-top: 24px; }

@media (max-width: 900px) {
  /* Reverted a fragile display:contents + order technique that used to
     be here, meant to move the notes panel earlier in the mobile scroll
     order. Confirmed BROKEN by a real screenshot: display:contents
     interacting with CSS Grid's auto-placement scattered the scrubber
     and its markers away from the video entirely, onto a completely
     different part of the page. That's a known, well-documented risk of
     this exact CSS pattern — flagged as unverifiable from this sandbox
     when it was written, and now confirmed to have actually failed.
     Back to the simple version: normal document order, single column,
     nothing reordered. Less convenient on mobile, but nothing here can
     silently scatter unrelated elements across the page. */
  .review-layout { grid-template-columns: 1fr; }
  .review-layout.share-review-layout { grid-template-columns: 1fr; }

  /* THE actual fix for the recurring "notes panel" issue. On mobile,
     .review-main and .comment-panel land in separate grid rows (single
     column, so they can't share one row side by side) — each row sizes
     to its own content, and total content height can easily and
     correctly exceed 100vh once there's any real number of notes. A
     fixed height:100vh with overflow:hidden — necessary for the
     desktop two-column layout above — clips anything past that
     boundary instead of letting the page scroll, which is exactly why
     more notes made this visibly worse: more content pushed further
     past the clip line. Letting height grow with content and overflow
     show normally here restores ordinary page scrolling on mobile,
     which is what should happen once nothing is trying to fit two
     independently-scrolling columns into one screen anymore. */
  .review-app-shell { height: auto; overflow: visible; }

  /* .comment-panel is height:100vh on desktop — correct there, since it
     sits in a SIDE-BY-SIDE column next to .review-main, both meant to
     fill one full viewport height independently. On mobile, where
     .review-layout above just switched to a single column (stacking
     .review-main then .comment-panel vertically instead of side by
     side), that same height:100vh means the notes panel still claims a
     FULL extra viewport's worth of height positioned right after the
     video — which is what "the notes panel covers the player" actually
     was: not real overlap, but the panel forcing itself to be as tall
     as the whole screen regardless of how much content it actually has.
     This override was in an earlier version of this file and was lost
     when a different, broken mobile technique was reverted nearby —
     dropped along with it by mistake, not deliberately. */
  .comment-panel { height: auto; }

  /* This is the actual missing piece. .app-shell is `display:flex` with
     no flex-direction — defaulting to `row`, correct for desktop (sidebar
     on the left, main content filling the rest). The mobile sidebar
     below claims width:100%, which cannot coexist with .main in a row
     layout: .main (flex:1, min-width:0) gets squeezed toward zero width
     while flexbox's default cross-axis stretch makes the now-empty-
     looking sidebar box stretch to match .main's full content height —
     which is exactly the "empty page, nav bar stuck near the bottom"
     bug a real screenshot confirmed. Column stacks them normally
     instead: sidebar bar on top, full-width main content below it. */
  .app-shell { flex-direction: column; }

  /* The sidebar used to just be display:none on mobile — full stop, no
     brand, no way to navigate at all once past the first page. Now it
     becomes a horizontal top bar (brand + toggle button) with the nav
     links collapsed behind the toggle instead of removed outright.
     Kept deliberately simple — push-down expansion via flex-wrap, not
     an absolutely-positioned overlay — given a previous CSS layout
     technique in this exact file (see the removed block above) turned
     out to be broken in a way I couldn't see from this sandbox; this
     is the lower-risk version of the same idea. */
  .sidebar {
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    padding: 10px 16px;
  }
  .sidebar-brand { padding: 0; border-bottom: none; margin-bottom: 0; flex: 1; }
  .sidebar-menu-toggle {
    display: inline-flex;
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    padding: 6px 12px;
    font-size: 1.1rem;
    cursor: pointer;
  }
  .nav-item {
    display: none;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    width: 100%;
    text-align: left;
    padding: 9px 20px;
    margin-bottom: 0;
    font-size: 0.9rem;
  }
  .sidebar.menu-open .nav-item { display: flex; }
  .nav-icon { display: none; }

  /* The bug: width:340px anchored via right:0 relative to the
     TRIGGER's own position, not the viewport. Below this width, if
     the trigger sits anywhere left of center — true on every page
     here, since review.ejs has no sidebar at all and a completely
     different header layout than the other six pages, so there's no
     single reliable trigger position to anchor to — a 340px dropdown
     extending left from that point runs off the left edge of the
     screen entirely. Anchoring to the viewport instead — fixed
     position, small side margins — makes this correct regardless of
     where the trigger actually sits on any given page. */
  .notif-bell-dropdown {
    position: fixed;
    top: 64px;
    left: 12px;
    right: 12px;
    width: auto;
    max-height: calc(100vh - 90px);
  }

  /* Notes and Captions are each long enough sections that showing both
     fully expanded, stacked one after another, makes the page unwieldy
     to scroll through on a small screen — this is what a real
     screenshot showed. A simple two-button switcher instead of trying
     to fit both at once; .mobile-tab-hidden's actual hiding behavior
     only exists here, so it can never affect desktop no matter what
     JS adds the class to. */
  .mobile-panel-tabs { display: flex; gap: 8px; margin: 16px 0; }
  .mobile-panel-tabs .mobile-panel-tab {
    flex: 1;
    background: var(--panel-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 8px 16px;
    font-size: 0.88rem;
    cursor: pointer;
  }
  .mobile-panel-tabs .mobile-panel-tab.active { background: var(--gold); color: var(--on-accent); border-color: var(--gold); font-weight: 600; }
  .mobile-tab-hidden { display: none !important; }

  /* Tour tooltip on mobile: a fixed bottom sheet instead of trying to
     position it precisely next to whatever element is highlighted.
     The JS still computes a top/left position (harmless — it's simply
     never used here), but on a small screen there often isn't reliable
     room near every possible target, and getting pixel-perfect
     positioning right near a viewport edge is exactly the kind of thing
     this codebase has gotten wrong before. A guaranteed, fixed location
     removes that risk entirely rather than trusting the math. !important
     is deliberate: the JS sets top/left via inline style on this same
     element, and an inline style silently wins over a plain media-query
     rule — the exact bug class already found once in this app — so
     !important is what actually guarantees this override takes effect. */
  .tour-tooltip {
    left: 12px !important;
    right: 12px !important;
    bottom: 12px !important;
    top: auto !important;
    width: auto !important;
    max-width: none !important;
    max-height: 50vh !important;
    overflow-x: hidden !important;
  }
  .tour-buttons { flex-direction: column; align-items: stretch; }
  .tour-buttons > div { display: flex; gap: 8px; }
  .tour-buttons > div > button { flex: 1; }
}

/* Upload page "Did You Know" tips panel. The upload form is
   deliberately narrow (max-width:520px, for readability), which on a
   wide desktop screen leaves a large, genuinely empty area to its
   right — reported directly. This fills that space with short,
   rotating feature tips, but ONLY while an upload is actively in
   progress (JS toggles .is-active on this element in sync with
   #progressWrap — see upload.ejs) and ONLY on screens wide enough to
   actually have the room for it, matching this file's own 900px
   breakpoint used everywhere else, inverted (shown above it rather
   than hidden below it). Content is admin-editable — see the "Upload
   Tips" tab in the admin panel and /admin/upload-tips.

   Sizing is almost entirely JS-driven (see updateDykCardHeight in
   upload.ejs), not fixed here, because every dimension requested is
   relative to something else on the page that only JS can measure:
   the card is sized from the upload FORM's own rendered height, the
   eyebrow header is sized to match the "Uploading as X" box's height
   so the two columns' second rows start on the same line, and the
   whole panel's total height is set to match the left column's total
   height so the collage (a flex child with flex:1) naturally fills
   whatever's left and its bottom lands exactly on the form's bottom —
   a direct, robust consequence of flexbox distributing space, not a
   value computed by replicating every margin in JS. */
.upload-layout { display: flex; gap: 28px; align-items: flex-start; }
.upload-main-col { display: flex; flex-direction: column; }
.did-you-know-panel { display: none; }
@media (min-width: 901px) {
  .did-you-know-panel.is-active {
    display: flex;
    flex-direction: column;
    flex-shrink: 0;
    position: sticky;
    top: 32px;
  }
}
.did-you-know-panel .dyk-eyebrow {
  font-size: 1.35rem;
  font-weight: 700;
  color: var(--gold-soft);
  display: flex;
  align-items: center;
  margin-bottom: 16px; /* matches #uploadingAsBox's own margin-bottom, so the form and the card start on the same line */
  flex-shrink: 0;
}
.did-you-know-panel .dyk-card {
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 28px;
}
.did-you-know-panel .dyk-icon { font-size: 3.4rem; margin-bottom: 18px; line-height: 1; }
.did-you-know-panel .dyk-title { font-size: 1.5rem; font-weight: 700; margin-bottom: 14px; line-height: 1.3; }
.did-you-know-panel .dyk-text { font-size: 1.1rem; color: var(--text-muted); line-height: 1.6; }
.did-you-know-panel .dyk-dots { flex-shrink: 0; display: flex; gap: 6px; margin: 18px 0; justify-content: center; }
.did-you-know-panel .dyk-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--border); transition: background 200ms ease; padding: 0; border: none; cursor: pointer; }
.did-you-know-panel .dyk-dot.active { background: var(--gold); }
@keyframes dykFadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.did-you-know-panel .dyk-card.dyk-entering { animation: dykFadeIn 350ms ease; }
@media (prefers-reduced-motion: reduce) {
  .did-you-know-panel .dyk-card.dyk-entering { animation: none; }
}

/* Scrub-frame collage — requested directly, beneath the tip card: a
   4-column, 5-row grid (landscape, matching the panel's own 16:9
   orientation) of frames pulled from this same user's own past
   uploads' scrub-preview sprite sheets (see the /my-scrub-frames
   route), each cell quietly fading to a different random frame over
   time. Empty (no child cells) and therefore visually nothing when a
   user has no past uploads yet — the collage fails safe by simply
   never being populated, not by showing broken image cells; see
   loadScrubCollage in upload.ejs.

   flex:1 (not a fixed or aspect-ratio-derived height) is what makes
   the bottom of the grid land exactly on the bottom of the upload
   form — it grows to fill whatever space remains in the panel after
   the eyebrow, card, and dots, once JS has set the panel's own total
   height to match the left column. grid-template-rows: repeat(5, 1fr)
   is what lets the row heights actually stretch to fill that space,
   rather than sizing themselves from content. */
.scrub-collage {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(5, 1fr);
  gap: 8px;
}
.scrub-collage-cell {
  border-radius: 4px;
  background-color: var(--panel-2);
  background-repeat: no-repeat;
  border: 1px solid var(--border);
  opacity: 0;
  transition: opacity 900ms ease;
  min-height: 0;
}
.scrub-collage-cell.visible { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .scrub-collage-cell { transition: none; }
}

/* --- Advertising ---------------------------------------------------
   Three independent placements (see services/ads.js). Each reserves its
   unit's exact size so an ad loading in never shoves the layout, and each
   is labelled — an ad should never be mistaken for the app's own UI.
   Hidden placements are display:none, which is what public/js/ads.js
   checks before it will mount a Google unit into one. */
.ad-unit { text-align: center; }
.ad-label {
  font-size: 0.66rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: 6px;
}
.ad-unit img {
  display: block;
  margin: 0 auto;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 4px;
}
.ad-google-slot { display: inline-block; min-width: var(--ad-w); min-height: var(--ad-h); }

/* Upload page, desktop: takes the scrub-frame grid's place in the side
   panel — which itself only appears while an upload is running. */
.upload-ad-slot { display: none; }
@media (min-width: 901px) {
  .did-you-know-panel.has-ad .scrub-collage { display: none; }
  .did-you-know-panel.has-ad .upload-ad-slot {
    display: flex;
    flex: 1 1 auto;
    min-height: 0;
    /* Top-aligned, directly under the tip card: centred in the leftover
       space it sat 100+px lower, below the fold on a 900px-tall window. */
    align-items: flex-start;
    justify-content: center;
    padding-top: 14px;
  }
  /* No tips to show: just the ad, at its natural height. */
  .did-you-know-panel.ad-only .dyk-eyebrow,
  .did-you-know-panel.ad-only .dyk-card,
  .did-you-know-panel.ad-only .dyk-dots { display: none; }
  .did-you-know-panel.ad-only .upload-ad-slot { flex: 0 0 auto; }
}

/* Upload page, phones: a small banner right below the form. The gap is
   deliberate — an ad pressed against the Upload button invites
   accidental taps, which AdSense treats as a policy problem. */
.ad-upload-mobile { display: none; }
@media (max-width: 900px) {
  .ad-upload-mobile { display: block; margin-top: 28px; }
}

/* Review page, desktop: bottom of the notes column, below the notes.
   .comment-panel is a flex column with .comment-list { flex: 1 }, so a
   non-shrinking footer sits in the lower-right corner while the notes
   list shrinks above it. Hidden on short windows, where 250px would
   squeeze the notes to nothing. */
.ad-review-corner { display: none; }
@media (min-width: 901px) and (min-height: 720px) {
  .ad-review-corner {
    display: block;
    flex: 0 0 auto;
    padding: 10px 16px 14px;
    border-top: 1px solid var(--border);
    background: var(--panel);
  }
  /* The floating Tour button is pinned to the very corner the ad now
     occupies (bottom 20px, right 20px) and was sitting on top of it —
     caught in a real-browser screenshot, not by the geometry checks. A
     control covering an ad is a viewability problem and invites
     accidental clicks, so on pages that show this ad it rides above it. */
  body:has(.ad-review-corner) .tour-start-btn { bottom: 330px; }
}

/* Spelling check: the folded-away groups (run-together words, probably-misread text, already dealt with). */
.spelling-folded { border: 1px solid var(--border); border-radius: var(--radius); margin-top: 12px; padding: 8px 12px; background: var(--panel); }
.spelling-folded > summary { cursor: pointer; font-weight: 600; color: var(--text); padding: 4px 0; }
.spelling-folded[open] > summary { margin-bottom: 4px; }
