:root {
  /* Near-black rather than literal #000 -- true black flattens elevation (surface/
     line stop reading as "a step up" once there's no room left below them) and can
     smear/halo on OLED displays; this stays close enough to read as black while
     keeping that headroom. Fully neutral (no hue bias) throughout this block, on
     purpose -- the previous cool/blue-leaning grays are gone. */
  --bg: #0a0a0a;
  --surface: #161616;
  --line: #2a2a2a;
  --ink: #ece9e1;
  --muted: #999999;
  /* Reserved for one meaning only: "pending / needs attention" in the status
     traffic-light (waiting on a reply, in progress, needs pickup, maybe-available,
     candidate date) -- never used as a plain UI highlight. Interactive chrome
     (buttons, active tags, focus rings, nav highlight) is monochrome, built from
     --ink/--bg instead, so color on this page always means something about the
     underlying data, never just "this is clickable" or "this is important". */
  --amber: #e9a23b;

  /* Semantic roles, split out from --amber so it can stay a single meaning
     instead of also doing duty for "problem" and "fact worth knowing" -- see the
     color plan artifact. */
  --success: #7bc86c;
  --danger: #e2726a;
  --info: #4fb0c9;

  /* Per-episode accent, one per episode number (1-6, fixed for this series).
     Was previously three bare rgba() values inline on the Scenes table only;
     promoted here so every view that groups/colors by episode reuses the
     same six colors. */
  --ep1: #4d9de0;
  --ep2: #9b6de0;
  --ep3: #e06da8;
  --ep4: #5ec4b4;
  --ep5: #c8785a;
  --ep6: #8c96dc;

  --ui: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;

  /* Text color for use on top of a solid accent fill (amber/success/danger/info) --
     one token instead of the same near-black hardcoded on every filled badge/button. */
  --on-accent: #14120c;

  /* Two sizes cover almost everything outside headings: -sm for labels, captions,
     badges and table cells; -base for everything else (buttons, inputs, tags, body
     copy). Headings get their own scale below -- see h1-h4. */
  --text-sm: 12px;
  --text-base: 14px;

  /* Two radii: -sm for form fields (a slightly tighter shape marks "type here" vs
     "click here"), -md for everything clickable/containing -- buttons, tags, cards,
     panels. Rectangular with a subtle round, not a pill. */
  --radius-sm: 4px;
  --radius-md: 6px;
  /* Reserved for floating/overlay surfaces only (the toast/action-sheet from the
     interaction-polish pass) -- iOS sheets read as noticeably rounder than a
     button or tag, and everything above keeps its existing radius unchanged. */
  --radius-lg: 14px;

  /* Shadow, motion and glow tokens for the visual-polish pass -- ambient/soft, never a
     hard skeuomorphic drop shadow. Two layers (tight+dark close to the edge, broad+soft
     further out) reads as real light rather than a flat offset rectangle. */
  --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.35);
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3), 0 6px 16px -8px rgba(0, 0, 0, 0.5);
  --shadow-md: 0 2px 4px rgba(0, 0, 0, 0.3), 0 12px 28px -10px rgba(0, 0, 0, 0.55);
  --shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.35), 0 24px 48px -16px rgba(0, 0, 0, 0.6);
  --glow-mono: 0 4px 20px -4px rgba(236, 233, 225, 0.4);
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);
  --fast: 140ms;
  --base: 220ms;

  /* One material (translucent + blurred), three intensities depending on how far
     off the page a given surface sits. Resting: always-on-screen surfaces holding
     real content (cards, sticky sidebars) -- lightest touch so body copy stays as
     readable as it is today. Active: "this is selected/true/do this" -- short,
     high-confidence text, can afford a stronger fill. Overlay: floats on top of
     everything, temporarily (menus, toast, confirm sheet) -- heaviest blur, since
     these are the furthest off the ground. See the design pitch for the full
     reasoning; .btn-primary/.tag.active/.nav-highlight/etc. below are what
     actually consume these. */
  --glass-fill-resting: color-mix(in srgb, var(--ink) 6%, transparent);
  --glass-border-resting: color-mix(in srgb, var(--ink) 14%, transparent);
  --glass-blur-resting: blur(10px) saturate(140%);

  --glass-fill-active: color-mix(in srgb, var(--ink) 16%, transparent);
  --glass-fill-active-strong: color-mix(in srgb, var(--ink) 24%, transparent);
  --glass-border-active: color-mix(in srgb, var(--ink) 30%, transparent);
  --glass-blur-active: blur(16px) saturate(180%);

  --glass-fill-overlay: color-mix(in srgb, var(--ink) 11%, transparent);
  --glass-border-overlay: color-mix(in srgb, var(--ink) 22%, transparent);
  --glass-blur-overlay: blur(24px) saturate(180%);

  color-scheme: dark;
}

/* Light mode -- toggled manually via the topbar's theme-toggle button (see
   app/static/js/theme-toggle.js), not automatic OS-preference detection; the app is
   dark-first, this is the alternate. Every rule elsewhere in this file already reads
   colors through these tokens rather than hardcoding hex values, so overriding just
   this block re-themes the whole app -- semantic accents (amber/success/danger/info)
   are darkened here rather than reused as-is, since the original values were tuned
   for contrast against a near-black background and read too light/washed-out as text
   on a pale one; --on-accent flips to a light color to match, since every accent
   token is also used as a solid fill with text on top of it (tags, flash messages,
   segmented-control thumbs) and a dark fill needs light text, not dark. */
:root[data-theme="light"] {
  --bg: #ede9e0;
  --surface: #f8f6f1;
  --line: #d9d3c5;
  --ink: #211d17;
  --muted: #635c4d;
  --amber: #a35f14;
  --success: #2f7d42;
  --danger: #b23b30;
  --info: #1f7a91;
  --ep1: #2f6fa8;
  --ep2: #6b459e;
  --ep3: #a8447a;
  --ep4: #2f8577;
  --ep5: #935a3f;
  --ep6: #5c63a8;
  --on-accent: #fffaf0;
  --shadow-xs: 0 1px 2px rgba(30, 25, 15, 0.08);
  --shadow-sm: 0 1px 2px rgba(30, 25, 15, 0.06), 0 6px 16px -8px rgba(30, 25, 15, 0.14);
  --shadow-md: 0 2px 4px rgba(30, 25, 15, 0.08), 0 12px 28px -10px rgba(30, 25, 15, 0.18);
  --shadow-lg: 0 4px 8px rgba(30, 25, 15, 0.1), 0 24px 48px -16px rgba(30, 25, 15, 0.22);
  --glow-mono: 0 4px 20px -4px rgba(33, 29, 23, 0.28);

  color-scheme: light;
}

* { box-sizing: border-box; }

/* The custom :active press-states throughout this file (search "press feel") are
   the deliberate replacement for this -- without suppressing it, tapping anything
   on a touchscreen also flashes the browser's own gray highlight rectangle behind
   whatever :active is doing, which reads as "web page", not "app". */
html { -webkit-tap-highlight-color: transparent; }

/* Opts every same-origin page-to-page navigation into the View Transitions API --
   in browsers that support it for cross-document navigation, the outgoing and
   incoming page crossfade instead of just cutting, and any element sharing a
   view-transition-name (the active nav pill, see .nav-highlight) visibly slides/
   morphs from its old position to its new one across the navigation. Elsewhere
   (or on a browser without support) this is a no-op -- a normal, instant page load,
   exactly like before. */
@view-transition {
  navigation: auto;
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
  /* The pseudo-elements the View Transitions API generates aren't matched by the
     universal selector above (they sit outside the normal DOM tree), so they need
     their own opt-out. */
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation-duration: 0.01ms !important;
  }
}

/* Otherwise a disabled field (rendered for anyone without edit rights on that
   resource -- see can_edit() throughout the templates) looks identical to a live
   one: same background, same border, same text color. Nothing here distinguished
   "you can't touch this" from "go ahead" until now. (A readonly .editable-field is
   deliberately excluded -- it already gets its own borderless, blends-as-plain-text
   treatment below, which makes the same point without dimming the prose itself.) */
input:disabled, select:disabled, textarea:disabled, button:disabled {
  opacity: 0.55;
  cursor: not-allowed;
}

input[type="text"], input[type="search"], input[type="tel"], input[type="email"], input[type="password"], input[type="date"], select {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  font-family: inherit;
  font-size: var(--text-base);
}

input[type="checkbox"] { accent-color: var(--ink); }

body {
  margin: 0;
  font-family: var(--ui);
  /* Flat by default -- body[data-ep] below adds an episode-colored wash on pages
     that have one clear episode home. The original reasoning for flat (a gradient
     read as a visible tint/smudge on this dark a background) was really about one
     uniform hue everywhere with no shape to it, not about gradients as a category
     -- a gradient's whole nature is variation, and tied to something that already
     means something here (the episode), it answers that complaint more directly
     than plain darkness did. */
  background: var(--bg);
  background-attachment: fixed;
  color: var(--ink);
  line-height: 1.4;
  font-size: var(--text-base);
  font-variant-numeric: tabular-nums;
  /* Stops a pull-down at the top of the page from handing off to the browser's own
     pull-to-refresh/rubber-band -- that handoff is one of the more obvious "this is
     a browser tab" tells on a touchscreen. Internal scrollers that DO want native
     momentum feel (the filters drawer, the script viewer's own pane) opt back in
     locally with -webkit-overflow-scrolling: touch where they're defined. */
  overscroll-behavior-y: contain;
}

/* The Ground layer's episode wash -- a diagonal sweep of the page's own episode
   color (via --ep-color, set generically by [data-ep="N"] further down; body gets
   its data-ep from base.html's body_ep block, wired up on the few pages that have
   one unambiguous episode home: Scene detail, Shot Planner, the script viewer).
   Resolves to plain --bg by the far edge, and fixed to the viewport like the base
   rule above, so it reads as atmosphere behind the page rather than a graphic
   printed on it. No wash at all on pages with no single clear episode (list pages,
   Shoots, the database entity pages) -- flat ground stays the correct default
   there, not a gap. */
body[data-ep] {
  /* var(--ep-color, var(--ink)) rather than a bare var(--ep-color) -- if data-ep
     ever holds a value the [data-ep="N"] rules below don't recognize (script
     viewer's selected_episode could in principle be an episode number this show
     doesn't have --epN..6 tokens for), an unresolved custom property inside
     color-mix() makes the whole background invalid, not just untinted -- body
     would render with no background at all instead of just skipping the wash. */
  background: linear-gradient(135deg, color-mix(in srgb, var(--ep-color, var(--ink)) 16%, transparent) 0%, transparent 60%), var(--bg);
  background-attachment: fixed;
}

/* Genuinely missing until now -- every place "muted" text reads gray today does so by
   inheriting color from an already-gray ancestor (a styled <h3>, a <td>); a standalone
   .muted with no such parent (most empty-state text, several legends/captions) was
   silently rendering at full --ink brightness the whole time. */
.muted { color: var(--muted); }
p.muted { animation: fade-in-up 320ms var(--ease); }
@keyframes fade-in-up {
  from { opacity: 0; transform: translateY(3px); }
  to { opacity: 1; transform: translateY(0); }
}

h1, h2, h3, h4 { font-family: inherit; font-weight: 700; line-height: 1.25; }
h1 { font-size: 24px; margin: 0 0 4px; }
h2 { font-size: 19px; margin: 0 0 4px; }
h3 { font-size: 16px; margin: 0 0 4px; }
h4 { font-size: var(--text-base); margin: 0 0 4px; }

/* Visible only for keyboard focus (not a mouse click) -- one consistent ring instead
   of relying on each browser's own default, without adding a focus ring to every
   mouse click the way a plain :focus rule would. */
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible,
select:focus-visible, summary:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.topbar {
  padding: 16px 24px;
  /* Installed standalone (see manifest.json), the topbar sits flush against the
     iPhone's status bar/notch with nothing between them -- env() falls back to 0
     everywhere else (a plain browser tab, Android, desktop), so this is additive
     padding only where a safe area actually exists. */
  padding-top: max(16px, env(safe-area-inset-top));
  padding-left: max(24px, env(safe-area-inset-left));
  padding-right: max(24px, env(safe-area-inset-right));
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  gap: 24px;
  position: sticky;
  top: 0;
  z-index: 10;
  background: var(--bg);
  transition: box-shadow var(--base) var(--ease), background var(--base) var(--ease), backdrop-filter var(--base) var(--ease);
}

.topnav { display: flex; align-items: center; gap: 16px; flex: 1; }
.topnav a { color: var(--muted); text-decoration: none; font-size: var(--text-sm); }
.topnav a:hover { color: var(--ink); }

.nav-divider { width: 1px; height: 14px; background: var(--line); }

/* Progress collapses to just its icon on desktop (title attr covers the label);
   margin-left: auto pushes it (and Shoots after it) to the right edge of the nav,
   separating "browse the database" links on the left from utility/primary-action
   ones on the right -- usual nav-bar convention. */
.nav-icon-link { display: flex; align-items: center; color: var(--muted); margin-left: auto; }
.nav-icon-link:hover { color: var(--ink); }

/* "Database" groups Locations/Characters/Costumes/Props/People into one nav slot,
   collapsing into a floating menu rather than five separate top-level links --
   usual nav-bar submenu shape. Plain button + class-toggle (nav.js), same
   reasoning as .nav-toggle/.filters-drawer-toggle: no native disclosure state to
   fight across breakpoints. */
.nav-dropdown { position: relative; }
.nav-dropdown-toggle {
  display: flex;
  align-items: center;
  gap: 4px;
  background: none;
  border: none;
  padding: 0;
  color: var(--muted);
  font-family: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}
.nav-dropdown-toggle:hover { color: var(--ink); }
.nav-dropdown-toggle::after { content: "\25be"; font-size: 9px; transition: transform var(--fast) var(--ease); }
.nav-dropdown.is-open .nav-dropdown-toggle::after { transform: rotate(180deg); }
.nav-dropdown-menu {
  display: none;
  flex-direction: column;
  gap: 2px;
  position: absolute;
  top: calc(100% + 12px);
  left: 0;
  min-width: 150px;
  background: var(--glass-fill-overlay);
  backdrop-filter: var(--glass-blur-overlay);
  -webkit-backdrop-filter: var(--glass-blur-overlay);
  border: 1px solid var(--glass-border-overlay);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  padding: 6px;
  z-index: 20;
}
/* Unlike the Active tier, a solid fallback here is worth being explicit about
   rather than just letting the un-blurred color-mix tint stand alone -- a menu
   full of readable link text over a busy page is a real legibility risk without
   the blur actually doing its job, not just a slightly-less-nice fallback. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav-dropdown-menu { background: var(--surface); }
}
.nav-dropdown.is-open .nav-dropdown-menu { display: flex; animation: navdrop var(--fast) var(--ease); }
.nav-dropdown-menu a { padding: 6px 10px; border-radius: var(--radius-sm); }
@media (hover: hover) and (pointer: fine) {
  .nav-dropdown-menu a:hover { background: var(--bg); color: var(--ink); }
}
.nav-dropdown-menu a:active { background: var(--bg); color: var(--ink); }

/* Hamburger menu below the breakpoint -- the nav has no room to stay inline on a phone,
   so it collapses into a dropdown instead (an earlier horizontal-scroll fix worked but
   didn't feel native; this replaces it outright). Plain button + class toggle (nav.js),
   same reasoning as .filters-drawer-toggle -- no native disclosure state to fight. */
.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 4px;
  /* 44px is the iOS HIG comfortable-tap-target minimum -- this used to be 32px,
     fine with a mouse but tight to hit reliably with a thumb. */
  width: 44px;
  height: 44px;
  padding: 0;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  cursor: pointer;
  order: 2;
  transition: border-color var(--base) var(--ease);
}
.nav-toggle:hover { border-color: var(--muted); }
.nav-toggle span {
  display: block;
  width: 16px;
  height: 2px;
  background: var(--ink);
  border-radius: 1px;
  transition: transform var(--base) var(--ease), opacity var(--fast) var(--ease);
}
/* Hamburger -> X: the two outer bars rotate into an X through the middle bar's
   position, the middle bar just fades -- three lines, two transforms, no new markup. */
.nav-toggle.is-open span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
.nav-toggle.is-open span:nth-child(2) { opacity: 0; }
.nav-toggle.is-open span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 768px) {
  .topbar { flex-wrap: wrap; }
  .nav-toggle { display: flex; }
  /* Sits immediately left of the hamburger rather than floating alone between the
     brand and the menu -- the hamburger (order: 2) stays the rightmost icon. */
  .theme-toggle { order: 1; margin-left: auto; }
  .topnav {
    display: none;
    order: 4;
    flex-basis: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--line);
  }
  /* Capped and internally scrollable -- on a short viewport (a landscape phone, a
     small window) the drawer's own content can exceed the available height; without
     this it silently grows past the bottom of the screen with no way to reach
     whatever's cut off, "Select User" (the actual login entry point) included. */
  .topnav.is-open {
    display: flex;
    animation: navdrop var(--base) var(--ease);
    max-height: calc(100vh - 90px);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
  }
  /* Direct children only -- .whoami now lives at the bottom of this drawer (see
     base.html) and keeps its own distinct look (dot + name + "switch"), not the
     plain nav-link treatment every other link in here gets. */
  .topnav > a { padding: 10px 4px; font-size: var(--text-base); transition: color var(--fast) var(--ease); }
  .topnav a.nav-highlight { margin: 4px 0; text-align: center; }
  .nav-divider { display: none; }

  /* Same top-border-separator idiom the drawer itself uses to split off from the
     page above it -- keeps the user-select block reading as its own distinct
     section at the bottom of the menu, not just one more nav link. */
  .topnav .whoami {
    margin-top: 14px;
    padding: 14px 4px 0;
    border-top: 1px solid var(--line);
  }

  /* On mobile, "Database" isn't a floating dropdown -- there's no room and no need
     for an overlay inside an already-open full-width drawer. It just becomes a
     small non-interactive label with its five links always shown inline below it. */
  .nav-dropdown-toggle {
    pointer-events: none;
    padding: 10px 4px 2px;
    font-size: var(--text-sm);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  .nav-dropdown-toggle::after { content: ""; }
  /* The toggle isn't a real button here (see above, pointer-events: none) -- when
     its section is active, the pill belongs on the actual tappable link inside the
     menu below, not on this inert label, so the fill this class adds everywhere
     else is cancelled back to the plain category-label look. */
  .nav-dropdown-toggle.nav-highlight {
    background: none;
    color: var(--muted);
    font-weight: 400;
    padding: 10px 4px 2px;
    view-transition-name: none;
  }
  .nav-dropdown-menu {
    display: flex !important;
    position: static;
    border: none;
    box-shadow: none;
    padding: 0;
    background: none;
  }
  .nav-dropdown-menu a { padding: 10px 4px; font-size: var(--text-base); }
  .nav-icon-link { margin-left: 0; gap: 8px; }
}

@keyframes navdrop {
  from { opacity: 0; transform: translateY(-6px); }
  to { opacity: 1; transform: translateY(0); }
}

/* A hairline shadow that only appears once the page has actually scrolled (see nav.js) --
   separates the header from content without a permanent line competing with the
   bottom-border it already has at the top of the page. Frosted-glass translucency
   joins it at the same moment -- clean and flat for a page at rest, and only once
   content is actually passing underneath does the bar read as "floating over"
   rather than "part of" the page, the classic iOS nav-bar material. */
.topbar.is-scrolled {
  box-shadow: var(--shadow-sm);
  background: color-mix(in srgb, var(--bg) 72%, transparent);
  backdrop-filter: blur(20px) saturate(180%);
  -webkit-backdrop-filter: blur(20px) saturate(180%);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topbar.is-scrolled { background: var(--bg); }
}

/* Marks whichever section of the nav you're currently in -- called out the same way
   an active tag is elsewhere (solid fill), so it's easy to spot at a glance rather
   than blending in. Monochrome: a solid ink fill (reads as a bright pill in dark
   mode, a dark pill in light mode) rather than a color, since this is UI emphasis,
   not information. Applied to whichever link/button matches the current page (see
   base.html), not fixed to one nav item -- shares one view-transition-name so that,
   in browsers supporting the View Transitions API for cross-document navigation
   (Chrome/Edge as of this writing; a plain instant swap everywhere else, so nothing
   breaks where it's unsupported), the pill visibly slides from its old spot to its
   new one across the page load instead of just appearing there. */
.topnav .nav-highlight {
  color: var(--ink);
  background: var(--glass-fill-active-strong);
  border: 1px solid var(--glass-border-active);
  backdrop-filter: var(--glass-blur-active);
  -webkit-backdrop-filter: var(--glass-blur-active);
  border-radius: var(--radius-md);
  padding: 3px 10px;
  font-weight: 600;
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--ink) 20%, transparent);
  transition: box-shadow var(--base) var(--ease), transform var(--fast) var(--ease), background var(--base) var(--ease);
  view-transition-name: nav-pill;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topnav .nav-highlight {
    color: var(--bg);
    background: linear-gradient(155deg, color-mix(in srgb, var(--ink) 85%, var(--bg)), var(--ink) 55%, color-mix(in srgb, var(--ink) 90%, var(--bg)));
    border-color: transparent;
    box-shadow: none;
  }
}
/* Every property redeclared here, not just inherited from the base rule above --
   .nav-dropdown-menu a:hover (a separate, lower-specificity rule but one that also
   matches this element when the active item happens to be inside the Database
   dropdown) sets its own flat dark background, which would otherwise win here since
   this rule previously left background alone. Left alone, that collision turned the
   pill's fill black on hover while this rule's own light-on-glass text stayed
   light too -- unreadable either way this could go wrong. */
.topnav .nav-highlight:hover {
  color: var(--ink);
  background: var(--glass-fill-active-strong);
  border-color: var(--glass-border-active);
  backdrop-filter: var(--glass-blur-active);
  -webkit-backdrop-filter: var(--glass-blur-active);
  box-shadow: var(--glow-mono), inset 0 1px 0 color-mix(in srgb, var(--ink) 20%, transparent);
  transform: translateY(-1px);
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: none;
  color: var(--muted);
  cursor: pointer;
  position: relative;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease);
}
/* An invisible hit-area expansion rather than growing the visible circle -- a 26px
   ring would look oversized/empty next to the small icon it holds if grown to a
   real 44px, but the *tappable* area can still be that big. */
.theme-toggle::before {
  content: "";
  position: absolute;
  inset: -9px;
}
.theme-toggle:hover { color: var(--ink); border-color: var(--muted); }
/* Icon shown = the theme a click switches *to* -- sun by default (dark mode active,
   click for light), moon once light mode is active (click to go back to dark). */
.theme-toggle-icon.icon-moon { display: none; }
:root[data-theme="light"] .theme-toggle-icon.icon-sun { display: none; }
:root[data-theme="light"] .theme-toggle-icon.icon-moon { display: block; }

.whoami { display: flex; align-items: center; gap: 8px; font-size: var(--text-sm); }
.whoami a { color: var(--muted); text-decoration: none; }
.whoami a:hover { color: var(--ink); }
.whoami form { display: inline; }

.whoami-dot, .person-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.person-dot { margin-right: 6px; vertical-align: middle; }

.link-btn {
  background: none;
  border: none;
  color: var(--muted);
  font-size: var(--text-sm);
  cursor: pointer;
  padding: 0;
  text-decoration: underline;
}
.link-btn:hover { color: var(--ink); }

.drag-handle {
  cursor: grab;
  color: var(--muted);
  width: 18px;
  text-align: center;
  user-select: none;
}
.drag-reorder > [data-id].dragging { opacity: 0.4; }
.drag-reorder > [data-id] { cursor: default; }

.brand {
  display: inline-flex;
  align-items: center;
}

/* The wordmark is a masked shape filled with currentColor's underlying token
   (--ink) rather than an <img>, so it inverts correctly in light mode -- an <img>'s
   pixels are baked-in white and can't respond to a CSS theme switch at all. */
.brand-logo {
  display: block;
  height: 16px;
  aspect-ratio: 224.46 / 23.6;
  background-color: var(--ink);
  -webkit-mask: url('../img/wordmark.svg') no-repeat center / contain;
  mask: url('../img/wordmark.svg') no-repeat center / contain;
}
.scene-page {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 24px;
  padding: 24px;
  align-items: start;
}

@media (max-width: 768px) {
  .scene-page { grid-template-columns: 1fr; padding: 16px; gap: 16px; }
}

.filters {
  position: sticky;
  top: 88px; /* clears the now-sticky .topbar (~64px) plus a 24px gap */
  /* Resting float -- the lightest of the three glass intensities, since this
     holds real body copy (filter labels, tag lists) rather than a short callout.
     Same @supports-fallback pattern as the other two tiers throughout this file. */
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
  padding: 16px;
  box-shadow: var(--shadow-xs);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .filters { background: var(--surface); border-color: var(--line); }
}
/* Must come after the base .filters rule above, not just inside its own media
   query -- equal specificity (both are just `.filters`) means a tie is broken by
   source order, not by which one is wrapped in a matching @media, so this was
   previously losing to the unconditional `position: sticky` above even on mobile.
   The visible symptom: a still-sticky filters panel re-pinning itself to top:88px
   on every scroll tick, rendering on top of (and interleaved with) the scene rows
   flowing underneath it in the single-column mobile layout, instead of just
   scrolling away normally like every other static block on the page. */
@media (max-width: 768px) {
  .filters { position: static; }
}

.filters .search input {
  width: 100%;
  padding: 8px;
  margin-bottom: 16px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink);
}

.filter-group { margin-bottom: 18px; }

.filter-group h3 {
  font-size: var(--text-sm);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 8px;
}

.filter-group h3 .muted { text-transform: none; letter-spacing: normal; font-size: var(--text-sm); }

.filter-dropdown summary {
  cursor: pointer;
  font-size: var(--text-sm);
  color: var(--muted);
  padding: 4px 0;
  list-style: none;
}
.filter-dropdown summary::-webkit-details-marker { display: none; }
.filter-dropdown summary::after { content: " ▾"; }
.filter-dropdown[open] summary::after { content: " ▴"; }
.filter-dropdown summary:hover { color: var(--ink); }

.filter-dropdown-list {
  max-height: 220px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
  padding: 8px 0 0;
}

/* Above the mobile breakpoint, the filter groups stay permanently visible (today's
   desktop behavior) -- a plain button + class toggle (see nav.js), not a native
   <details>, since overriding a closed <details>'s hidden content needs the UA
   stylesheet's own display:none to lose a specificity fight, and that's a needlessly
   fragile thing to depend on when a plain div has no such native state to fight. */
.filters-drawer-toggle { display: none; }
@media (max-width: 768px) {
  .filters-drawer-toggle {
    display: block;
    width: 100%;
    text-align: left;
    background: none;
    border: none;
    cursor: pointer;
    font-family: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--ink);
    padding: 4px 0 12px;
  }
  /* Smooth open/close instead of a hard display:none/block snap -- a 0fr/1fr grid row
     animates height with no JS height-measuring, as long as the actual content sits in
     one overflow:hidden inner wrapper (.filters-drawer-body-inner). */
  .filters-drawer-body {
    display: grid;
    grid-template-rows: 0fr;
    transition: grid-template-rows var(--base) var(--ease);
  }
  .filters-drawer-body-inner { overflow: hidden; min-height: 0; }
  .filters-drawer-body.is-open { grid-template-rows: 1fr; }
}

.tag {
  display: inline-block;
  font-size: var(--text-sm);
  padding: 3px 8px;
  margin: 0 4px 4px 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  color: var(--muted);
  text-decoration: none;
  transition: color var(--fast) var(--ease), border-color var(--fast) var(--ease), background var(--fast) var(--ease), transform var(--fast) var(--ease);
}

/* hover: hover excludes touch -- without it, tapping a tag on a phone leaves it
   stuck in its :hover look until something else is tapped (touch fires :hover on
   tap but never a real "pointer left" to clear it). :active is the touch-native
   press feedback instead, on every device. */
@media (hover: hover) and (pointer: fine) {
  .tag:hover { color: var(--ink); border-color: var(--muted); }
}
.tag:active { transform: scale(0.96); }

/* On-set live-refresh indicator (see shoots/overview.html) -- a breathing dot turns an
   otherwise invisible 30s reload into something people notice and trust. */
.live-tag { display: inline-flex; align-items: center; gap: 6px; }
.live-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--success);
  box-shadow: 0 0 0 0 rgba(123, 200, 108, 0.6);
  animation: livepulse 2s var(--ease) infinite;
}
@keyframes livepulse {
  0% { box-shadow: 0 0 0 0 rgba(123, 200, 108, 0.5); }
  70% { box-shadow: 0 0 0 8px rgba(123, 200, 108, 0); }
  100% { box-shadow: 0 0 0 0 rgba(123, 200, 108, 0); }
}

.tag.active {
  color: var(--ink);
  background: var(--glass-fill-active);
  border-color: var(--glass-border-active);
  backdrop-filter: var(--glass-blur-active);
  -webkit-backdrop-filter: var(--glass-blur-active);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .tag.active { background: var(--ink); color: var(--bg); border-color: var(--ink); }
}

/* A fact worth knowing (e.g. "weather dependent"), not a problem needing attention --
   kept visually distinct from .tag.active's amber so the two meanings don't collide. */
.tag.tag-info {
  color: var(--on-accent);
  background: var(--info);
}

/* Marks which specific costume variation is linked, next to the still-recognizable
   master name -- a plain non-interactive annotation (not a link), so italic is enough
   to read as "extra detail" without a new color competing with .tag.active's amber. */
.costume-variation-note { font-style: italic; cursor: default; }

/* A variation only has two things to manage -- its own name and which scenes it
   applies to -- so it gets a small self-contained card on the master's own page
   rather than a page of its own. */
.variation-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.variation-card {
  /* Had no fill at all before -- brought in line with every other card in the
     Resting tier rather than left as the one exception. */
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
  padding: 10px 12px;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .variation-card { background: var(--surface); border-color: var(--line); }
}
.variation-card-header { display: flex; align-items: center; gap: 10px; }
.variation-card-header .editable-field { max-width: 280px; font-weight: 600; }
.variation-card-scenes { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 8px; }
.variation-scene-tag { display: inline-flex; align-items: center; gap: 4px; padding: 3px 4px 3px 8px; }
.variation-scene-tag form { display: inline-flex; }
.variation-scene-tag button {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: var(--text-base);
  line-height: 1;
  padding: 0 2px;
}
.variation-scene-tag button:hover { color: var(--ink); }
.variation-add-scene { display: flex; gap: 6px; margin-top: 8px; }
.variation-add-scene input { max-width: 160px; }

button.tag {
  background: none;
  font: inherit;
  font-size: var(--text-sm);
  cursor: pointer;
}

.clear-filters {
  display: inline-block;
  margin-top: 8px;
  font-size: var(--text-sm);
  color: var(--ink);
  text-decoration: underline;
}

/* A grid item's default min-width is auto, not 0 -- without this, .scene-list
   (the 1fr track in .scene-page's grid) refuses to shrink below its widest
   child's intrinsic content width, so the fixed-min-width table below blows out
   the whole page horizontally instead of scrolling locally inside .table-scroll
   the way it's actually meant to. */
.scene-list { min-width: 0; }

.scene-list-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 12px;
}

.count { color: var(--muted); font-size: var(--text-base); }

/* Every list page's table (Locations, Props, Costumes, Characters, People, Scenes,
   Shoots) shares this one rule -- Resting float here covers all of them at once. */
table {
  width: 100%; border-collapse: collapse; border-radius: var(--radius-md); overflow: hidden; box-shadow: var(--shadow-xs);
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  table { background: var(--surface); border-color: var(--line); }
}

th, td {
  text-align: left;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
  font-size: var(--text-base);
}

th.col-num, td.col-num { text-align: center; }

/* Keeps short, fixed-shape columns (a date, a status word, a scene count) from
   claiming more width than their content needs, on a row that also has to fit
   Locations/Cast -- lists of tags that genuinely can run long and are the ones
   actually worth giving the freed-up room to. Desktop-table-only: below the
   mobile breakpoint the row is a flex container (table.table-responsive
   tr:not([hidden]), further down), not a table row, and td.td-inline already has
   its own considered flex-shrink/min-width:0/ellipsis handling there -- stacking
   this `width: 1%` on top of that collapsed the Status/Scenes cells down to a
   few px wide instead (min-width: 0 removes the shrink floor, width: 1% gives
   the flex algorithm a near-zero basis to shrink from, and overflow: hidden then
   clips what's left to a sliver). */
@media (min-width: 769px) {
  th.shoots-col-tight, td.shoots-col-tight { width: 1%; white-space: nowrap; }
}

/* Scenes is the one list whose row-expand toggle is active on desktop too (every
   other list's toggle is td-toggle-mobile-only) -- revealing its hidden detail row
   used to make the browser's auto table-layout recompute every column's width from
   scratch, visibly shifting the whole table. Fixed widths (scoped here, not applied
   to every table.table-responsive, since other lists' column content doesn't need
   or want this) stop that: the hidden row no longer participates in sizing at all. */
@media (min-width: 769px) {
  /* Percentage columns only stay legible above a real floor -- the narrowest ones
     (Int/Ext, Length, both 8%) have header text ("Int/Ext" + its sort arrow)
     measuring ~77px, so below about 960px table width, 8% comes out narrower than
     the header text itself, which (nowrap + no ellipsis) just visibly spills into
     the neighboring column instead of wrapping or clipping. min-width holds the
     table at a width where every column comfortably fits its own header, and the
     .table-scroll wrapper (in the template) lets anything narrower than that
     scroll horizontally instead of overlapping. */
  .scene-list table.table-responsive { table-layout: fixed; min-width: 1040px; }
  .scene-list table.table-responsive th:nth-child(1) { width: 26%; }
  .scene-list table.table-responsive th:nth-child(2) { width: 8%; }
  .scene-list table.table-responsive th:nth-child(3) { width: 22%; }
  .scene-list table.table-responsive th:nth-child(4) { width: 10%; }
  .scene-list table.table-responsive th:nth-child(5) { width: 8%; }
  .scene-list table.table-responsive th:nth-child(6) { width: 21%; }
  .scene-list table.table-responsive th:nth-child(7) { width: 5%; }
}

.flash-messages { max-width: 900px; margin: 16px auto 0; padding: 0 24px; }
.flash {
  padding: 8px 14px;
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  margin-bottom: 8px;
}
.flash-error { background: var(--danger); color: var(--on-accent); }
.flash-message, .flash-success { background: var(--success); color: var(--on-accent); }

.photo-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 14px;
  margin-bottom: 14px;
}

/* Pinterest's widget always renders on its own white background (it's a third-party
   iframe, not themeable) -- a light card + radius keeps that from looking like a
   broken rectangle sitting directly on a dark page. Fixed min-height so the section
   doesn't collapse to nothing while pinit.js is still loading/rendering the widget
   in place of the plain <a> tag it starts as. */
.video-embed { margin-top: 8px; max-width: 640px; }
.video-embed iframe { display: block; width: 100%; aspect-ratio: 16 / 9; border: none; border-radius: var(--radius-md); }

.map-embed { margin-top: 12px; }
.map-embed iframe { display: block; width: 100%; height: 260px; border: none; border-radius: var(--radius-md); }

.pinterest-board-embed {
  margin-top: 8px;
  padding: 10px;
  background: #fff;
  border-radius: var(--radius-md);
  min-height: 90px;
  overflow: hidden;
}
.photo-item {
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
  overflow: hidden;
  padding-bottom: 6px;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .photo-item { background: var(--bg); border-color: var(--line); }
}
.photo-item img { width: 100%; height: 190px; object-fit: cover; display: block; cursor: zoom-in; }
.photo-caption { font-size: var(--text-sm); color: var(--muted); padding: 4px 8px 0; }
.photo-item form { padding: 2px 8px 0; }

.lightbox-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  /* Overlay tier's blur, kept separate from the fill tokens -- this scrim is
     already near-opaque on purpose (it's dimming the page to focus on one
     photo, not showing a menu's worth of text through it), so it only needs
     the blur half of the recipe, not a lighter/translucent fill too. */
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  cursor: zoom-out;
  z-index: 1000;
}
.lightbox-overlay[hidden] { display: none; }
.lightbox-overlay img {
  max-width: 100%;
  max-height: 100%;
  border-radius: var(--radius-md);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}

.video-lightbox-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  z-index: 1000;
}
.video-lightbox-overlay[hidden] { display: none; }
.video-lightbox-box {
  width: 100%;
  max-width: 800px;
  aspect-ratio: 16 / 9;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.video-lightbox-box iframe { display: block; width: 100%; height: 100%; border: none; }

.rough-cut-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  vertical-align: middle;
  margin-left: 4px;
  color: #ff0033;
  opacity: 0.85;
}
.rough-cut-icon:hover { opacity: 1; }
.rough-cut-icon svg { width: 15px; height: 15px; display: block; }

.photo-upload-form { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.photo-upload-form input[type="text"] {
  flex: 1;
  min-width: 140px;
  padding: 6px 8px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink);
}
.photo-upload-form input[type="file"] { font-size: var(--text-sm); color: var(--muted); }

.editable-field {
  display: block;
  width: 100%;
  max-width: 420px;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px;
  font-family: inherit;
  font-size: var(--text-base);
}
textarea.editable-field { width: 100%; max-width: none; min-height: 70px; resize: vertical; }
.editable-field:focus { border-color: var(--ink); outline: none; }
.save-indicator { font-size: var(--text-sm); color: var(--success); margin-left: 8px; }
.save-indicator.flash { animation: indicator-in var(--base) var(--ease); }

/* One-beat confirmation on anything that just saved over fetch -- a select, a textarea,
   an input -- so a silent network success still closes the loop visually. */
.save-flash { animation: save-flash 700ms var(--ease); }
@keyframes save-flash {
  0% { box-shadow: 0 0 0 0 rgba(123, 200, 108, 0.55); }
  100% { box-shadow: 0 0 0 7px rgba(123, 200, 108, 0); }
}
@keyframes indicator-in {
  from { opacity: 0; transform: translateY(2px); }
  to { opacity: 1; transform: translateY(0); }
}

.editable-field[readonly] {
  background: none;
  border-color: transparent;
  padding: 0;
  color: var(--ink);
  resize: none;
}
textarea.editable-field[readonly] { min-height: 0; }
.editable-field[readonly]::placeholder { color: var(--muted); }

.entity-form { display: flex; flex-direction: column; gap: 16px; max-width: 520px; margin-top: 16px; }
.entity-form > label { display: flex; flex-direction: column; gap: 6px; font-size: var(--text-base); color: var(--muted); }
.entity-form > label .muted { font-size: var(--text-sm); }
.entity-form .tag-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 2px; }

.entity-form input[type="text"],
.entity-form input[type="tel"],
.entity-form input[type="email"],
.entity-form input[type="password"],
.entity-form select,
.entity-form textarea {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px;
  font-family: inherit;
  font-size: var(--text-base);
}
.entity-form textarea { resize: vertical; }
.entity-form .form-actions { margin-top: 4px; }

.btn-primary {
  display: inline-block;
  background: var(--glass-fill-active-strong);
  backdrop-filter: var(--glass-blur-active);
  -webkit-backdrop-filter: var(--glass-blur-active);
  border: 1px solid var(--glass-border-active);
  /* A translucent fill no longer reads as "a light block" the way solid --ink did,
     so the text goes back to --ink (light-on-dark, like everywhere else) instead
     of --bg (dark-on-light) -- the old pairing assumed an opaque light background
     that isn't there anymore. */
  color: var(--ink);
  font-weight: 700;
  padding: 8px 16px;
  border-radius: var(--radius-md);
  cursor: pointer;
  text-decoration: none;
  font-size: var(--text-base);
  box-shadow: inset 0 1px 0 color-mix(in srgb, var(--ink) 20%, transparent);
  transition: box-shadow var(--base) var(--ease), transform var(--fast) var(--ease), background var(--base) var(--ease);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .btn-primary { background: var(--ink); color: var(--bg); border-color: transparent; box-shadow: none; }
}
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover { box-shadow: var(--glow-mono), inset 0 1px 0 color-mix(in srgb, var(--ink) 20%, transparent); transform: translateY(-1px); }
}
.btn-primary:active { transform: translateY(0) scale(0.97); }

/* WhatsApp's own brand green -- deliberately not --amber/--success, since this
   should read as "WhatsApp" specifically, not as this app's own accent/success
   color. Only ever rendered when a shoot actually has a link set. A small icon-only
   corner tag, not a full button -- it's a convenience link, not the page's primary
   action. */
.whatsapp-tag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  color: #25d366;
  text-decoration: none;
  position: relative;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.whatsapp-tag::before { content: ""; position: absolute; inset: -7px; }
.whatsapp-tag svg { display: block; }
.whatsapp-tag:hover { border-color: #25d366; background: rgba(37, 211, 102, 0.12); }
.whatsapp-tag:active { transform: scale(0.97); }
.btn-primary[disabled] { transform: none; box-shadow: none; }

.whatsapp-summary-result { margin-top: 10px; }
.whatsapp-summary-text {
  display: block;
  width: 100%;
  min-height: 120px;
  resize: vertical;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px;
  font-family: inherit;
  font-size: var(--text-base);
  white-space: pre-wrap;
}
.whatsapp-summary-actions { display: flex; align-items: center; gap: 12px; margin-top: 6px; }

th {
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  white-space: nowrap;
}

tbody tr:nth-child(even) { background: rgba(255, 255, 255, 0.025); }
.kv-table tbody tr:nth-child(even) { background: none; }

tbody tr.needs-review { background: rgba(233, 162, 59, 0.08); }
.dood-cell { color: var(--muted); }
.dood-cell-work { background: rgba(233, 162, 59, 0.14); color: var(--ink); font-weight: 600; }

/* "Find yourself" filter on the public Shoot Overview share page. */
.find-yourself-bar { margin: 14px 0; }
.find-yourself-bar input { max-width: 260px; }
tr.find-match { background: rgba(79, 176, 201, 0.14) !important; }
tr.find-dim { opacity: 0.35; }
tr.cut { opacity: 0.5; }
tr.past-shoot { opacity: 0.55; }
tr.past-shoot .row-link { color: var(--muted); }

/* Per-episode accent color, keyed off a data-ep="N" attribute -- every element
   below (Scenes table rows, episode tags, schedule blocks/timeline) sets
   --ep-color from the same six tokens, so the same episode always reads as
   the same color everywhere it shows up. */
[data-ep="1"] { --ep-color: var(--ep1); }
[data-ep="2"] { --ep-color: var(--ep2); }
[data-ep="3"] { --ep-color: var(--ep3); }
[data-ep="4"] { --ep-color: var(--ep4); }
[data-ep="5"] { --ep-color: var(--ep5); }
[data-ep="6"] { --ep-color: var(--ep6); }

/* Subtle per-episode accent, just enough to help the eye group rows while scrolling. */
tr[data-ep] td:first-child { border-left: 3px solid color-mix(in srgb, var(--ep-color) 55%, transparent); padding-left: 8px; }

.ep-tag { border-color: var(--ep-color) !important; color: var(--ep-color) !important; }
/* Same Active-float recipe as .tag.active/.btn-primary (16%/24% fill, 30% border,
   the shared blur), just tinted with this tag's own episode color instead of
   monochrome ink -- one material, worn in whatever color a given "active" state
   already means. Light ink text throughout (not --on-accent, which assumed an
   opaque, fully-saturated fill that isn't there anymore). */
.ep-tag.active {
  background: color-mix(in srgb, var(--ep-color) 24%, transparent) !important;
  border-color: color-mix(in srgb, var(--ep-color) 30%, transparent) !important;
  backdrop-filter: var(--glass-blur-active);
  -webkit-backdrop-filter: var(--glass-blur-active);
  color: var(--ink) !important;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .ep-tag.active { background: var(--ep-color) !important; border-color: transparent !important; color: var(--on-accent) !important; }
}

.ep-num, .ep-num:hover { color: var(--ep-color) !important; font-weight: 700; }

.epbar-fill[data-ep] {
  background: linear-gradient(90deg, color-mix(in srgb, var(--ep-color) 50%, black), var(--ep-color));
}

/* Shot state at or past "shot" -- dropdown and location both read as green. */
/* Full shot-state color scale, keyed off data-shot-state="<value>" -- was
   previously a single binary "progressed vs not" green. */
[data-shot-state="shot"], [data-shot-state="shot"] a,
[data-shot-state="locked"], [data-shot-state="locked"] a { color: var(--success) !important; }
[data-shot-state="needs_pickup"], [data-shot-state="needs_pickup"] a { color: var(--amber) !important; }
[data-shot-state="clean"], [data-shot-state="clean"] a { color: var(--info) !important; }
[data-shot-state="cut"], [data-shot-state="cut"] a { color: var(--danger) !important; text-decoration: line-through; opacity: 0.75; }
/* An otherwise-unshot scene that's already on a shoot day is worth calling out
   separately from the "nothing planned yet" default -- reuses --info since a scene
   can never be both "clean" (already shot) and "scheduled but unshot" at once, so the
   two meanings never compete for attention in the same place. */
[data-shot-state="unshot"][data-scheduled="true"], [data-shot-state="unshot"][data-scheduled="true"] a { color: var(--info) !important; }
select.shot-state-select[data-shot-state], select.onset-shot-select[data-shot-state] { border-color: currentColor; }

th.sortable { cursor: pointer; user-select: none; }
.sort-arrow { display: inline-block; margin-left: 4px; font-size: var(--text-sm); opacity: 0.35; }
th.sort-active .sort-arrow { opacity: 1; color: var(--ink); }

.mini-progress {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 100px;
}
.mini-progress .bar { flex: 1; height: 5px; background: var(--line); border-radius: var(--radius-sm); overflow: hidden; }
.mini-progress .bar-fill {
  height: 100%;
  background: linear-gradient(90deg, color-mix(in srgb, var(--success) 55%, black), var(--success));
  transition: width 900ms var(--ease);
}
.mini-progress .frac { font-size: var(--text-sm); color: var(--muted); white-space: nowrap; }

.cut-badge {
  display: inline-block;
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius-sm);
  padding: 1px 4px;
  margin-left: 4px;
}

.mono { font-variant-numeric: tabular-nums; }

.scene-location {
  color: var(--muted);
  font-size: var(--text-sm);
  margin-left: 8px;
  text-decoration: none;
}
a.scene-location:hover { color: var(--ink); text-decoration: underline; }

/* Length column, colored relative to every other scene's page length (tertiles,
   computed in scenes.list_scenes) -- green/short is good news for scheduling, red/long
   is worth a second look, same ascending traffic-light convention as everywhere else
   color implies "pay attention" rather than pass/fail. */
.length-short { color: var(--success); }
.length-mid { color: var(--amber); }
.length-long { color: var(--danger); }

.entity-page, .detail-page { max-width: 900px; margin: 0 auto; padding: 24px; }

.entity-page-header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  margin-bottom: 12px;
}

.row-link { color: var(--ink); text-decoration: none; font-weight: 600; }
.row-link:hover { text-decoration: underline; }

td.truncate {
  max-width: 320px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.back-link {
  display: inline-block;
  color: var(--muted);
  text-decoration: none;
  font-size: var(--text-sm);
  margin-bottom: 12px;
}
.back-link:hover { color: var(--ink); }

.scene-nav-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.scene-nav-row .back-link { margin-bottom: 0; }
.scene-nav-links { display: flex; gap: 16px; }

.detail-page h1 { margin-bottom: 4px; }
.detail-page .subtitle { color: var(--muted); margin-top: 0; }

.tag-row { margin: 8px 0 20px; }

.detail-section {
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

.detail-section h3 {
  font-size: var(--text-sm);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 10px;
}

.detail-section .notes { white-space: pre-wrap; font-size: var(--text-base); }

/* backdrop-filter reset too, not just background -- without it a kv-table would
   still blur whatever's behind it with no visible surface to justify the blur,
   inherited silently from the generic table rule above. */
.kv-table { width: auto; background: none; backdrop-filter: none; -webkit-backdrop-filter: none; border: none; }
.kv-table tr { border: none; }
.kv-table th {
  text-align: left;
  color: var(--muted);
  font-weight: 500;
  padding: 4px 20px 4px 0;
  border: none;
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--text-base);
  vertical-align: top;
}
.kv-table td { padding: 4px 0; border: none; font-size: var(--text-base); }

.login-page { max-width: 420px; margin: 60px auto; padding: 24px; text-align: center; }
.login-page h1 { margin-bottom: 4px; }
.login-page .muted { color: var(--muted); font-size: var(--text-base); margin-bottom: 20px; }
.login-page form { display: flex; gap: 8px; }

.login-page select {
  flex: 1;
  padding: 8px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--ink);
}

.progress-page { max-width: 720px; margin: 0 auto; padding: 24px; }

.progress-header { display: flex; align-items: flex-end; flex-wrap: wrap; gap: 20px; padding-bottom: 20px; border-bottom: 1px solid var(--line); }

.progress-header .bignum { position: relative; font-size: 44px; font-weight: 800; line-height: 0.9; color: var(--ink); }
.progress-header .bignum small { font-size: 0.4em; color: var(--muted); }
/* One soft glow, reserved for the number that matters most (shot %, not scheduled %) --
   a radial wash behind the digits, not a border or a box, so it reads as light rather
   than a badge. */
.progress-header .bignum.bignum-glow::before {
  content: "";
  position: absolute;
  inset: -26px -40px;
  background: radial-gradient(circle, color-mix(in srgb, var(--ink) 35%, transparent), transparent 70%);
  z-index: -1;
}

.inthecan { font-size: var(--text-sm); letter-spacing: 0.14em; color: var(--ink); margin-bottom: 4px; font-weight: 700; }
.inthecan-scheduled { color: var(--muted); }
.progress-header .bignum-secondary { font-size: 26px; color: var(--muted); }
.progress-meta .subline { color: var(--muted); font-size: var(--text-base); }

.weight-toggle { margin: 16px 0 24px; }

.progress-legend { display: flex; gap: 16px; margin-bottom: 12px; font-size: var(--text-sm); color: var(--muted); }
.progress-legend .legend-dot { margin-right: 6px; }

.episode-row {
  display: grid;
  grid-template-columns: 30px 1fr 3fr;
  align-items: center;
  gap: 12px;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
  font-size: var(--text-base);
}

.epno { color: var(--muted); }
.eppct { text-align: right; font-weight: 700; }
.epcount { color: var(--muted); text-align: right; font-size: var(--text-sm); }

.epbars { display: flex; flex-direction: column; gap: 5px; }
.epbar-line { display: grid; grid-template-columns: 1fr 44px 50px; align-items: center; gap: 12px; }

.epbar { height: 6px; background: var(--line); border-radius: var(--radius-sm); overflow: hidden; }
.epbar-fill { height: 100%; background: var(--ink); transition: width 900ms var(--ease); }
.epbar-fill-scheduled { background: linear-gradient(90deg, color-mix(in srgb, var(--muted) 60%, black), var(--muted)); }

select.shot-state-select, select.status-select, select.onset-shot-select {
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 4px 6px;
}

/* Location/Prop/Shoot status color, keyed off data-status-scope + data-status --
   a shared attribute name ("confirmed", "cut") means different things (final vs
   mid-progress) depending on which entity it's on, so scope is part of the selector. */
[data-status-scope="location"][data-status="not_contacted"] { color: var(--muted); }
[data-status-scope="location"][data-status="waiting_for_reply"] { color: var(--amber); }
[data-status-scope="location"][data-status="confirmed"] { color: var(--success); }
[data-status-scope="location"][data-status="cut"] { color: var(--danger); }

[data-status-scope="shoot"][data-status="planned"] { color: var(--muted); }
[data-status-scope="shoot"][data-status="confirmed"] { color: var(--amber); }
[data-status-scope="shoot"][data-status="completed"] { color: var(--success); }
[data-status-scope="shoot"][data-status="cancelled"] { color: var(--danger); }

[data-status-scope="prop"][data-status="needed"] { color: var(--muted); }
[data-status-scope="prop"][data-status="in_progress"] { color: var(--amber); }
[data-status-scope="prop"][data-status="ready"] { color: var(--success); }

[data-status-scope="shot-plan"][data-status="planned"] { color: var(--muted); }
[data-status-scope="shot-plan"][data-status="shot"] { color: var(--success); }
[data-status-scope="shot-plan"][data-status="cut"] { color: var(--danger); text-decoration: line-through; }

/* Shot Planner (scenes/shot_planner.html): a dedicated, bespoke workspace for
   directors to block out a scene's coverage -- separate from the Scene detail page,
   which only shows a light summary + a link here. Sidebar layout mirrors
   .scene-page/.filters (sticky reference card, same 88px sticky-topbar offset
   convention); the shot grid mirrors .photo-gallery's auto-fill card pattern. */
.shot-planner-layout {
  display: grid;
  grid-template-columns: 280px 1fr;
  gap: 24px;
  padding: 24px;
  align-items: start;
}
@media (max-width: 768px) {
  .shot-planner-layout { grid-template-columns: 1fr; padding: 16px; gap: 16px; }
}

.shot-planner-sidebar {
  position: sticky;
  top: 88px;
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
  padding: 16px;
  box-shadow: var(--shadow-xs);
  display: flex;
  flex-direction: column;
  gap: 14px;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .shot-planner-sidebar { background: var(--surface); border-color: var(--line); }
}
@media (max-width: 768px) {
  .shot-planner-sidebar { position: static; }
}

.shot-planner-board-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.shot-list-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
  gap: 16px;
}

.shot-card {
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  /* A resting shadow, not just one that appears on interaction -- this is a real
     card floating over the board (draggable, its own edit-toggle scope), and
     should read as one at a glance, not only once something happens to it. */
  box-shadow: var(--shadow-xs);
  transition: box-shadow var(--base) var(--ease);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .shot-card { background: var(--surface); border-color: var(--line); }
}
.shot-card:focus-within { box-shadow: var(--shadow-sm); }

.shot-card-header { display: flex; align-items: center; gap: 8px; }
.shot-card-number { color: var(--muted); font-weight: 700; }
.shot-card-header .status-select { margin-left: auto; max-width: 120px; }

.shot-field-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}
@media (min-width: 480px) {
  .shot-field-grid { grid-template-columns: repeat(4, 1fr); }
}
.shot-field-grid select, .shot-field-grid input { width: 100%; }

.shot-character-picker { display: flex; flex-wrap: wrap; gap: 6px; border: none; padding: 0; margin: 0; }
/* An author-set `display` always beats the UA stylesheet's `[hidden] { display:
   none }`, regardless of specificity -- origin order, not the specificity contest,
   decides between them. Same fix already used for .lightbox-overlay[hidden] above. */
.shot-character-picker[hidden] { display: none; }
.character-pill { position: relative; cursor: pointer; }
.character-pill input[type="checkbox"] { position: absolute; opacity: 0; pointer-events: none; }
.character-pill:has(input:disabled) { cursor: default; }

.shot-photo-links { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
.shot-photo-upload { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 6px; }
.shot-photo-upload input[type="text"] { max-width: 160px; }

[data-status-scope="availability"][data-status="yes"] { color: var(--success); }
[data-status-scope="availability"][data-status="no"] { color: var(--danger); }
[data-status-scope="availability"][data-status="maybe"] { color: var(--amber); }

select.status-select[data-status] { border-color: currentColor; }
.tag[data-status] { border-color: currentColor; }

/* A <select> has no readonly attribute -- disabled is the only native way to lock
   it, but a disabled select still looks like a live dropdown (box, arrow) via the
   status-select/shot-state-select rules above. .toggle-select opts a select into
   the same edit-toggle-driven lock as .editable-field, without actually being
   .editable-field -- that class also wires up editable-field.js's own blur-save,
   which would double-fire alongside a select's existing change-based save
   (status-select.js / scenes.js). This blends a disabled select into plain text
   the same way [readonly] does above. Placed after the two currentColor border
   rules above (same specificity, so source order decides) so a status-coded
   select's border still gets cleared while disabled rather than staying visible;
   deliberately no `color` override, so a [data-status]/[data-shot-state] select
   keeps its status color while disabled instead of flattening to plain ink. */
select.toggle-select:disabled {
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border-color: transparent;
  padding: 0;
  opacity: 1;
  cursor: default;
}

.tag-select { margin: 0 4px 4px 0; }

.tag-input.editable-field {
  display: inline-block;
  width: 100px;
  max-width: 100px;
  margin: 0 4px 4px 0;
  padding: 4px 6px;
}

.schedule-timeline {
  display: flex;
  width: 100%;
  height: 40px;
  margin-bottom: 12px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  border: 1px solid var(--line);
}
.schedule-timeline-segment {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 4px;
  background: var(--ink);
  border-right: 1px solid var(--bg);
  overflow: hidden;
}
.schedule-timeline-segment.is-note { background: var(--line); }
.schedule-timeline-segment[data-ep] { background: var(--ep-color); }
.schedule-timeline-segment span {
  font-size: var(--text-sm);
  color: var(--on-accent);
  white-space: nowrap;
  padding: 0 4px;
}

.flag-row { display: flex; gap: 18px; align-items: center; margin-top: 2px; }
.flag-toggle-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--text-sm);
  color: var(--muted);
  cursor: pointer;
}
/* These three are genuinely on/off switches (unlike the character-picker's tag
   pills), so they get the actual switch shape -- built entirely on the existing
   checkbox (no new markup), keeping its native keyboard/space-to-toggle behavior.
   Checked state fills solid --ink/--bg like every other "active" state in this
   file (.tag.active, the nav pill) rather than reaching for --success -- color
   here is reserved for saying something about the underlying data, not for a
   generic "this is on" that has nothing to do with the show itself. */
.flag-toggle-label input[type="checkbox"] {
  appearance: none;
  -webkit-appearance: none;
  position: relative;
  width: 36px;
  height: 22px;
  margin: 0;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--surface);
  cursor: pointer;
  flex-shrink: 0;
  transition: background var(--base) var(--ease), border-color var(--base) var(--ease);
}
.flag-toggle-label input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--muted);
  transition: transform var(--base) var(--ease), background var(--base) var(--ease);
}
.flag-toggle-label input[type="checkbox"]:checked {
  background: var(--glass-fill-active-strong);
  border-color: var(--glass-border-active);
  backdrop-filter: var(--glass-blur-active);
  -webkit-backdrop-filter: var(--glass-blur-active);
}
.flag-toggle-label input[type="checkbox"]:checked::after {
  transform: translateX(14px);
  background: var(--ink);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .flag-toggle-label input[type="checkbox"]:checked { background: var(--ink); border-color: var(--ink); }
  .flag-toggle-label input[type="checkbox"]:checked::after { background: var(--bg); }
}

/* Printing a call sheet: dark theme wastes ink and reads poorly on paper, so flip to
   plain black-on-white and drop every control that only makes sense on screen. */
@media print {
  body { background: #fff; color: #000; }
  .topbar, .back-link, .flash-messages,
  .edit-toggle, .btn-primary, .link-btn, form { display: none !important; }
  .detail-section { border-top-color: #ccc; }
  .tag { border-color: #999; color: #333; }
  .tag.active { background: #ddd; color: #000; }
  table { border-color: #999; background: none; }
  th, td { border-bottom-color: #ccc; color: #000; }
  th { color: #555; }
  a { color: #000; }
}

.location-summary + .location-summary {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* Generic "show more" toggle for a table row (row-expand.js): rotates in place rather
   than swapping glyphs, and reveals either inline .td-secondary fields, a following
   .row-expand-detail row, or both at once. */
.row-expand-toggle {
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: var(--text-sm);
  padding: 2px 4px;
  display: inline-block;
  position: relative;
  transition: color var(--fast) var(--ease), transform var(--base) var(--ease);
}
.row-expand-toggle::before { content: ""; position: absolute; inset: -12px; }
@media (hover: hover) and (pointer: fine) {
  .row-expand-toggle:hover { color: var(--ink); }
}
tr.is-expanded .row-expand-toggle { transform: rotate(90deg); }
/* max-height/overflow live on .row-expand-detail-inner (a plain <div> added in the
   template), not directly on td.notes -- a table CELL mostly ignores max-height for
   layout purposes (the table row-sizing algorithm sizes it to content regardless,
   a real cross-browser table-layout limitation, confirmed by measuring computed
   max-height:0 actually rendering at ~99px here), even though the exact same rule
   works fine on mobile once the row reflows to display:flex for the stacked-card
   layout and td becomes an ordinary flex item. A plain block-level div inside the
   cell isn't subject to that limitation either way, so this animates correctly at
   every width instead of only below the mobile breakpoint. (An earlier attempt used
   display:grid + a 0fr row track directly on the td for the same reason .filters-
   drawer-body uses that trick -- same table-cell issue: it measured an extra,
   unexplained implicit row instead of the one collapsing track.) 600px comfortably
   covers any real action summary; the transition still looks right for shorter
   content, it just finishes growing before the max-height figure itself would. */
tr.row-expand-detail td { background: var(--bg); padding: 0 14px; }
tr.row-expand-detail td.notes {
  font-size: var(--text-base);
  margin: 0;
  padding-top: 0;
  padding-bottom: 0;
  transition: padding var(--base) var(--ease);
}
tr.row-expand-detail.is-open td.notes { padding-top: 10px; padding-bottom: 10px; }
.row-expand-detail-inner {
  overflow: hidden;
  max-height: 0;
  transition: max-height var(--base) var(--ease);
}
tr.row-expand-detail.is-open .row-expand-detail-inner { max-height: 600px; }

/* Month-grid calendar (app/calendar_utils.py) -- currently just the Availability
   candidate-dates picker, written generically so a future Shoots-on-a-calendar view
   can reuse the same grid/day classes. */
.calendar-header {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-bottom: 10px;
}
.calendar-header h3 { margin: 0; min-width: 160px; text-align: center; font-size: var(--text-base); }
.calendar-nav-btn {
  color: var(--ink);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 2px 10px;
}
.calendar-nav-btn:hover { border-color: var(--ink); }

.calendar-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 4px;
}
.calendar-weekday {
  text-align: center;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  padding-bottom: 4px;
}
.calendar-day {
  aspect-ratio: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--bg);
  padding: 4px;
  transition: border-color var(--base) var(--ease), box-shadow var(--base) var(--ease);
}
.calendar-day-num { color: var(--muted); font-size: var(--text-base); }
.calendar-day.is-other-month { opacity: 0.35; }
.calendar-day.is-past { opacity: 0.35; }
.calendar-day.is-candidate { border-color: var(--amber); }

/* A day with a shoot grows to fit its card instead of staying a cramped square --
   every other (empty) day keeps the compact aspect-ratio:1 tile. */
.calendar-day.has-shoot {
  aspect-ratio: auto;
  align-items: stretch;
  justify-content: flex-start;
  /* Resting float only for days that actually hold something -- an empty day is
     structural grid, not a card, and stays plain ground-colored like today. */
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border-color: var(--glass-border-resting);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .calendar-day.has-shoot { background: var(--surface); border-color: var(--line); }
}
@media (hover: hover) and (pointer: fine) {
  .calendar-day.has-shoot:hover { border-color: var(--muted); box-shadow: var(--shadow-xs); }
}
.calendar-day.has-shoot .calendar-day-num { align-self: flex-start; }

/* Day/Evening mini-toggles per calendar cell -- independent candidate slots, so a
   director can say (for instance) yes to shooting evenings on a date without also
   opening it up for a full day. */
.calendar-slot-toggles { display: flex; gap: 3px; }
.calendar-slot-toggles form { margin: 0; }
.calendar-slot-btn {
  width: 20px;
  height: 20px;
  background: none;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  color: var(--muted);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1;
  position: relative;
}
/* A real 44px hit area would overlap the Day/Evening slot sitting right next to
   this one in the same cramped calendar cell -- this is the most this particular
   layout can safely give without turning adjacent taps into a coin flip. */
.calendar-slot-btn::before {
  content: "";
  position: absolute;
  inset: -4px;
}
.calendar-slot-btn:hover { border-color: var(--ink); color: var(--ink); }
.calendar-slot-btn.is-active { background: var(--amber); color: var(--on-accent); }

/* Real-Shoot marker on the public Calendar tab's month grid -- a small card with
   at-a-glance info (status, scene count), colored by shot status like everywhere
   else in the app, rather than an unlabeled dot. */
.calendar-shoot-card {
  display: flex;
  flex-direction: column;
  gap: 1px;
  width: 100%;
  margin-top: 4px;
  padding: 4px 6px;
  border-radius: var(--radius-sm);
  border: 1px solid currentColor;
  background: color-mix(in srgb, currentColor 14%, transparent);
  text-decoration: none;
  font-size: var(--text-sm);
  line-height: 1.3;
  overflow: hidden;
  transition: background var(--fast) var(--ease);
}
.calendar-shoot-card:hover { background: color-mix(in srgb, currentColor 26%, transparent); }
.calendar-shoot-card-status {
  font-weight: 700;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.calendar-shoot-card-meta { color: var(--muted); white-space: nowrap; }

.table-scroll { overflow-x: auto; }

/* Row-per-card responsive table -- three field tiers, so a card shows what matters for
   scanning many rows at once and tucks the rest behind a tap, rather than forcing every
   column into its own full-width line regardless of how many rows that costs:
     .td-card-title   the row's identity -- always visible, always first (order: -2).
     .td-inline       "at a glance" fields -- sit on the title's own line, wrapping onto
                      a second line only if they don't fit; no auto label (they read as
                      compact badges/values, not label:value pairs).
     .td-secondary    everything else -- data-label="..." still renders its label:value
                      line, but the field is display:none until the row gains
                      .is-expanded (see .row-expand-toggle / row-expand.js).
   A row with no .td-secondary fields at all is effectively tier 3 ("one line, like
   desktop") for free -- just mark every field .td-inline and skip the toggle column.
   A td with none of these three classes (tier 1, rare) keeps the original stacked
   label:value line -- the fallback for a table that hasn't opted into tiering. */
@media (max-width: 768px) {
  /* backdrop-filter reset alongside background -- on mobile the card treatment
     moves to individual tr elements below, so the table container itself should
     have no visible surface at all, blur included (otherwise it'd blur whatever's
     behind it with nothing visible to justify that). */
  table.table-responsive { display: block; background: none; backdrop-filter: none; -webkit-backdrop-filter: none; border: none; box-shadow: none; }
  table.table-responsive thead { display: none; }
  table.table-responsive tbody { display: block; }
  table.table-responsive tr[hidden] { display: none; }
  table.table-responsive tr:not([hidden]) {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    column-gap: 8px;
    row-gap: 6px;
    background: var(--glass-fill-resting);
    backdrop-filter: var(--glass-blur-resting);
    -webkit-backdrop-filter: var(--glass-blur-resting);
    border: 1px solid var(--glass-border-resting);
    border-radius: var(--radius-md);
    padding: 10px 10px;
    margin-bottom: 10px;
    box-shadow: var(--shadow-sm);
    transition: box-shadow var(--base) var(--ease), transform var(--base) var(--ease);
  }
  @supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
    table.table-responsive tr:not([hidden]) { background: var(--surface); border-color: var(--line); }
  }
  @media (hover: hover) and (pointer: fine) {
    table.table-responsive tr:not([hidden]):hover {
      box-shadow: var(--shadow-md);
      transform: translateY(-2px);
    }
  }
  /* Touch equivalent of the hover lift above -- a quick press-down instead of a
     lift, since a phone has no hover to lift "toward" in the first place. */
  table.table-responsive tr:not([hidden]):active {
    box-shadow: var(--shadow-xs);
    transform: scale(0.99);
  }
  /* The episode-color left-border (desktop tables) becomes a top gradient edge on a
     stacked card instead -- same signal, shape that fits the card. */
  table.table-responsive tr[data-ep] {
    position: relative;
    overflow: hidden;
    padding-top: 13px;
  }
  table.table-responsive tr[data-ep]::before {
    content: "";
    position: absolute;
    top: 0; left: 0; right: 0;
    height: 3px;
    background: linear-gradient(90deg, var(--ep-color), color-mix(in srgb, var(--ep-color) 40%, transparent));
  }

  /* Default (untiered) field: the original full-width label:value line. */
  table.table-responsive td {
    display: flex;
    flex: 1 0 100%;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    padding: 5px 0;
    border: none;
    text-align: right;
  }
  table.table-responsive td::before {
    content: attr(data-label);
    flex: 1 0 auto;
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--muted);
    text-align: left;
  }

  table.table-responsive td.td-mobile-hide { display: none; }

  table.table-responsive td.td-card-title {
    order: -3;
    flex: 1 1 auto;
    min-width: 0;
    /* Pushes every later sibling on this line (inline fields, the toggle) over to the
       right edge as a group, instead of everything bunching up against the title with
       empty space left on the right -- the usual "title left, meta right" layout. */
    margin-right: auto;
    justify-content: flex-start;
    font-size: var(--text-base);
    font-weight: 700;
    text-align: left;
    overflow: hidden;
  }
  table.table-responsive td.td-card-title > * { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  table.table-responsive td.td-card-title::before { content: none; }

  /* Sits on the title's line, compact, no label. order: -2 (between the title's -3 and
     secondary fields' default 0, and ahead of the toggle's -1) so it stays put right
     after the title even once the row expands and reveals order-0 secondary fields --
     its position doesn't depend on where it happens to sit in the underlying column
     order or in the DOM. */
  table.table-responsive td.td-inline {
    order: -2;
    flex: 0 1 auto;
    min-width: 0;
    padding: 0;
    text-align: right;
    overflow: hidden;
  }
  table.table-responsive td.td-inline::before { content: none; }
  /* Shrinkable content inside an inline field, so a long person name or a wide progress
     bar compresses (or ellipsizes) before the row ever wraps its toggle onto an orphan
     line by itself -- the toggle (flex: 0 0 auto, never shrinks) should always be the
     one guaranteed-stable-width thing on the line. */
  table.table-responsive td.td-inline .tag {
    display: inline-block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    vertical-align: bottom;
  }
  table.table-responsive td.td-inline .mini-progress { min-width: 0; flex-shrink: 1; }
  table.table-responsive td.td-inline .mini-progress .bar { min-width: 28px; flex-shrink: 1; }
  /* A native <select>'s displayed value doesn't wrap or shrink like a tag's text does --
     without this, a long option (e.g. "Waiting For Reply") is exactly what strands the
     toggle onto its own line. */
  table.table-responsive td.td-inline select {
    max-width: 30vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* Collapsed by default; .row-expand-toggle (row-expand.js) adds .is-expanded to the
     row. Each forces its own full-width line, with a divider only above the first one
     so the expanded block reads as one group under the summary line. */
  table.table-responsive td.td-secondary { display: none; }
  table.table-responsive tr.is-expanded td.td-secondary {
    display: flex;
    border-top: 1px solid var(--line);
    padding-top: 8px;
    margin-top: 2px;
  }
  table.table-responsive tr.is-expanded td.td-secondary ~ td.td-secondary {
    border-top: none;
    padding-top: 5px;
    margin-top: 0;
  }
  /* A long free-text field (.truncate on desktop, where it's clipped to one line since
     the full text is a click away) is the whole reason someone expanded the card --
     stack its label above the full, wrapped text instead of the usual right-aligned
     label:value line, and drop the desktop clipping now that there's room. */
  table.table-responsive td.td-secondary.truncate {
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    white-space: normal;
    overflow: visible;
    text-overflow: clip;
    max-width: none;
    text-align: left;
  }
  table.table-responsive td.td-secondary.truncate::before { text-align: left; }

  /* The expand toggle itself -- trailing end of the line (order -1: after the title and
     any inline fields, still ahead of secondary fields' default 0), the conventional
     spot for a row's disclosure arrow. Its DOM position doesn't need to match -- it can
     live wherever is most natural in the markup (see each table's <thead> for why).
     Present on both breakpoints for a table whose toggle also reveals real extra
     content on desktop (Scenes' action summary); .td-toggle-mobile-only hides it above
     the breakpoint for tables where it only ever does something on mobile. */
  table.table-responsive td.td-toggle { order: -1; flex: 0 0 auto; padding: 0; }
  table.table-responsive td.td-toggle::before { content: none; }
}
@media (min-width: 769px) {
  .td-toggle-mobile-only { display: none; }
  .mobile-only { display: none; }
}
@media (max-width: 768px) {
  /* For a bare value (a count, a number) that only needs a unit/word of context once its
     data-label is suppressed inline on mobile -- e.g. "3" on desktop, "3 scenes" inline. */
  .desktop-only { display: none; }
}

/* Bulk select-and-set toolbar above the availability submission table. */
.bulk-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-bottom: 10px;
  padding: 8px 12px;
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .bulk-actions { background: var(--surface); border-color: var(--line); }
}
.bulk-actions button[disabled] { opacity: 0.4; cursor: default; }
@media (max-width: 480px) {
  .bulk-actions button { flex: 1 1 auto; white-space: nowrap; }
}

/* 3-way Yes/Maybe/No segmented control on the availability submission page. */
/* A single thumb slides behind whichever button is active instead of each button
   carrying its own fill -- the thumb's position/color is driven entirely by the
   segmented control's own data-value attribute (already kept in sync by
   availability-form.js), so no extra JS is needed for the animation itself. */
.segmented {
  position: relative;
  display: inline-flex;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: 3px;
  gap: 2px;
}
.segmented button {
  position: relative;
  z-index: 1;
  flex: 1;
  background: none;
  color: var(--muted);
  border: none;
  padding: 6px 14px;
  font-family: inherit;
  font-size: var(--text-base);
  cursor: pointer;
  transition: color var(--base) var(--ease);
}
.segmented button:hover { color: var(--ink); }
.segmented button.active { color: var(--on-accent); font-weight: 600; }
.seg-thumb {
  position: absolute;
  top: 3px;
  bottom: 3px;
  left: 3px;
  width: calc(33.333% - 3.333px);
  border-radius: 4px;
  opacity: 0;
  z-index: 0;
  transition: transform var(--base) var(--ease), background var(--base) var(--ease), opacity var(--fast) var(--ease);
}
.segmented[data-value="yes"] .seg-thumb { opacity: 1; background: var(--success); transform: translateX(0%); }
.segmented[data-value="maybe"] .seg-thumb { opacity: 1; background: var(--amber); transform: translateX(100%); }
.segmented[data-value="no"] .seg-thumb { opacity: 1; background: var(--danger); transform: translateX(200%); }

/* Schedule tab's cast/directors cell: plain colored names (no pill/border) for the
   common no-note case, so a scene with 5-6 confirmed people doesn't read as a wall of
   bordered chips -- a note gets its own line below, since that's the case actually
   worth the extra visual weight. */
.availability-name-list { line-height: 1.6; }
.availability-name { font-weight: 600; }
.availability-person-note { font-size: var(--text-sm); font-style: italic; margin-top: 2px; }

.availability-legend {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 14px;
  font-size: var(--text-base);
}
.availability-legend > span { display: inline-flex; align-items: center; gap: 6px; }

/* One dot style shared by every legend (Availability, Progress): defaults to
   currentColor so it can just sit inside a colored [data-status] element, or take an
   explicit fill via a modifier class -- the compound selector keeps the modifier
   winning regardless of each stylesheet section's order. */
.legend-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; display: inline-block; }
.legend-dot.legend-dot-shot { background: var(--ink); }
.legend-dot.legend-dot-scheduled { background: var(--muted); }

/* Schedule tab: each date is a <details> (default collapsed in markup -- no `open`
   attribute), with the possible-scene count colored red-to-green (inline hue, computed
   server-side in _schedulable_dates) so it's readable without expanding anything. */
.schedule-date { padding: 0; overflow: hidden; }
.schedule-date summary {
  cursor: pointer;
  list-style: none;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 14px 16px;
}
.schedule-date summary::-webkit-details-marker { display: none; }
.schedule-date summary::before {
  content: "▸";
  color: var(--muted);
  font-size: var(--text-sm);
  transition: transform 0.15s;
}
.schedule-date[open] summary::before { transform: rotate(90deg); }

/* One of these per active slot (day/evening) within an expanded date. */
.schedule-slot-section { margin: 0 16px 16px; }
.schedule-slot-heading {
  margin: 0 0 8px;
  font-size: var(--text-sm);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink);
}
.schedule-slot-section table { width: 100%; }

.scene-count-badge {
  font-size: var(--text-sm);
  padding: 2px 10px;
  border-radius: var(--radius-md);
}

.schedule-bulk-bar {
  position: sticky;
  /* Same formula as .script-toc-panel -- has to track .topbar's own
     safe-area-aware height (padding-top: max(16px, env(safe-area-inset-top)))
     rather than a flat guess, or this ends up hidden half-under the topbar on a
     notched phone instead of clearing it. This one stays sticky on every
     viewport (no mobile override), so it's the one place this would have been
     most visible. */
  top: calc(max(16px, env(safe-area-inset-top)) + 69px);
  z-index: 5;
}

.is-claimed-elsewhere { text-decoration: line-through; opacity: 0.4; }
.is-claimed-elsewhere .scene-select { cursor: not-allowed; }

/* Phase 5: read-only script viewer. Its own monospace stack (system fonts only, no
   new external dependency) so a scene's script visually reads as a script, distinct
   from the rest of the site's single-font UI. */
:root {
  --mono-script: ui-monospace, 'SF Mono', 'Courier New', Courier, monospace;
}

.script-page {
  font-family: var(--mono-script);
  font-size: var(--text-base);
  line-height: 1.6;
  max-width: 640px;
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
  padding: 24px clamp(16px, 4vw, 48px);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .script-page { background: var(--surface); border-color: var(--line); }
}
/* A scene can split into more than one .script-page box when a transition falls
   mid-scene (rare, but handled) rather than at the very end -- give the boxes
   breathing room from each other the same way .script-transition-divider already
   gets from its own margin. */
.script-page + .script-page { margin-top: 20px; }
.script-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.02em;
  margin-bottom: 14px;
}
.script-heading-meta { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; }
/* Metadata, not script text -- deliberately breaks out of the heading's mono/bold/
   uppercase/tracked styling so it reads as a subtle aside, not part of the slugline. */
.script-heading-director {
  font-family: var(--ui);
  font-size: var(--text-sm);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--muted);
  white-space: nowrap;
}
.script-heading-scheduled {
  font-family: var(--ui);
  font-weight: 400;
  text-transform: none;
  letter-spacing: normal;
  color: var(--info);
  border-color: var(--info);
  white-space: nowrap;
}
.script-heading-scheduled:hover { background: color-mix(in srgb, var(--info) 15%, transparent); }
.script-block-group { margin-bottom: 2px; }
/* .script-block wraps every line in the block (a run of action, or one character's
   cue + parenthetical(s) + dialogue) -- the "+ note" toggle anchors to the whole
   block, not to any one line inside it, since commenting is per-block. */
.script-block { position: relative; padding: 3px 34px 3px 14px; margin: -3px -8px -3px -14px; }
.script-line { margin: 0; }
.script-action { color: var(--ink); }
.script-character { margin-top: 14px; margin-left: 30%; font-weight: 700; }
.script-parenthetical { margin-left: 24%; max-width: 45%; color: var(--muted); font-style: italic; }
.script-dialogue { margin-left: 14%; max-width: 60%; }
.script-transition { font-weight: 700; }

/* Screenplay margins are conventionally page-relative, but on a narrow phone screen
   the same percentages leave dialogue only a couple of words wide before wrapping --
   lighten the indent and give the right edge much more room to breathe. */
@media (max-width: 640px) {
  .script-character { margin-left: 14%; }
  .script-parenthetical { margin-left: 10%; max-width: 78%; }
  .script-dialogue { margin-left: 4%; max-width: 92%; }
}

/* A transition ("HARD CUT TO:") is connective tissue between two beats, not part of
   either one -- not commentable, and rendered *outside and between* .script-page
   boxes (their own padding and fill already demarcate a scene clearly) rather than
   squeezed inside one. Plain muted text, no rules -- the surrounding boxes already
   do the demarcating. */
.script-transition-divider {
  font-family: var(--mono-script);
  font-size: var(--text-base);
  line-height: 1.6;
  margin: 16px 0;
  text-align: right;
  color: var(--muted);
  max-width: 640px;
}
.script-transition-divider .script-transition { color: var(--muted); }
/* When a scene's script-page ends in a transition, that divider already marks the
   boundary -- don't also stack the generic inter-scene gap on top of it, or the
   space below the transition ends up roughly 3x the space above it. */
.script-browse-scene:has(> .script-transition-divider:last-child) + .script-browse-scene {
  margin-top: 0;
}

/* Sluglines and character names can be clickable (to a Location / Character / Scene
   page), but should read exactly like ordinary script text -- no blue-link styling,
   no button chrome. A faint dotted underline on hover is the only hint. */
.script-heading a, .script-character a {
  color: inherit;
  text-decoration: none;
}
.script-heading a:hover, .script-character a:hover {
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
}

.script-note-toggle {
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  line-height: 16px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  /* No fill, matching every other small icon button in the app (theme toggle, nav
     toggle, the WhatsApp link) -- this one had picked up a stray var(--surface)
     that its siblings never had, not a deliberate Active-tier callout. */
  background: none;
  color: var(--muted);
  font-size: 12px;
  cursor: pointer;
  opacity: 0;
  transition: opacity var(--fast) var(--ease);
}
.script-block-group:hover .script-note-toggle,
.script-note-toggle:focus-visible { opacity: 1; }
@media (max-width: 768px) {
  .script-note-toggle { opacity: 1; } /* no hover on touch -- always show the tap target */
}
.script-note-toggle:hover { border-color: var(--ink); color: var(--ink); }

/* Hovering a block that can actually be commented on (i.e. it has a "+ note" toggle
   at all -- logged out visitors get none) shades it faintly, so it's unambiguous
   exactly which stretch of text the toggle belongs to. */
.script-block-group:has(.script-note-toggle):hover .script-block {
  background: color-mix(in srgb, var(--ink) 5%, transparent);
  border-radius: var(--radius-sm);
}

/* Highlights the logged-in user's own character's dialogue, anywhere they read the
   script -- an inset accent bar rather than a plain background so it keeps showing
   even while the block is also being hovered for a comment (that state sets its own
   background on the same element, see above). */
.script-block-mine .script-block {
  background: color-mix(in srgb, var(--amber) 6%, transparent);
  border-radius: var(--radius-sm);
  box-shadow: inset 3px 0 0 var(--amber);
}

/* Annotations sit inline in the script flow, not in a monospace font -- they're
   commentary, not script text, and should read as clearly distinct from it.
   --depth (0 = top-level comment, capped at MAX_REPLY_DEPTH server-side) pushes a
   reply progressively further right than its parent -- a long chain stops indenting
   further once it hits the cap, instead of marching off the page. */
.script-annotations, .script-annotation-children { margin: 0; }
.script-annotation {
  font-family: var(--ui);
  font-size: var(--text-sm);
  margin: 4px 0 4px calc(14% + var(--depth, 0) * 22px);
  padding: 4px 0 4px 10px;
  border-left: 2px solid var(--annotation-color, var(--line));
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
}
.script-annotation-date { margin-left: auto; }
.script-annotation-reply-toggle { font-size: var(--text-sm); }
.script-annotation-delete {
  background: none;
  border: none;
  padding: 0;
  color: var(--muted);
  font-size: var(--text-sm);
  cursor: pointer;
  text-decoration: underline;
}
.script-annotation-delete:hover { color: var(--danger); }

/* A cancel/close affordance next to a primary action -- an "x" glyph rather than a
   text button, so it doesn't compete with "Post"/"Reply" for attention. */
.icon-btn {
  background: none;
  border: none;
  padding: 0;
  color: var(--muted);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}
.icon-btn:hover { color: var(--ink); }

.script-note-form[hidden] { display: none; }
.script-note-form:not([hidden]) {
  margin: 6px 0 10px calc(14% + var(--depth, 0) * 22px);
  max-width: 60%;
  min-width: 240px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px;
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-sm);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .script-note-form:not([hidden]) { background: var(--bg); border-color: var(--line); }
}
.script-note-form textarea {
  min-height: 50px;
  resize: vertical;
  font-family: var(--ui);
  font-size: var(--text-sm);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 8px;
}
.script-note-form textarea:focus { border-color: var(--ink); outline: none; }
.script-note-form-actions { display: flex; align-items: center; gap: 14px; }

/* Sides packet (shoots/sides.html): every scheduled scene's script back to back --
   one scene per printed page. */
.sides-scene + .sides-scene {
  margin-top: 32px;
  padding-top: 24px;
  border-top: 1px dashed var(--line);
}

@media print {
  .script-note-toggle, .script-annotation-delete { display: none !important; }
  .sides-scene + .sides-scene { page-break-before: always; border-top: none; margin-top: 0; padding-top: 0; }
}

/* Script browse page (script/browse.html): whole-episode reader, wider than the
   standard .detail-page/.entity-page cap since it holds a TOC alongside the script
   column. */
.script-browse-page { max-width: 1120px; margin: 0 auto; padding: 24px; }
.script-browse-page > h1 { margin: 0 0 16px; }
.script-browse-controls { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; }
.script-browse-controls select {
  max-width: 100%;
  background: var(--bg);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
  font-family: inherit;
  font-size: var(--text-base);
}
/* Mobile-only scene jump dropdown, sitting next to the episode select -- the sidebar
   TOC (see @media below) doesn't fit well as a horizontal strip once an episode has
   20+ scenes, so mobile gets a dropdown instead of a row of pills. */
.script-toc-mobile { display: none; }

.script-browse-layout { display: flex; align-items: flex-start; gap: 32px; }
/* The episode select, comments toggle, and scene TOC all live in one sticky column
   so they stay visible together while the script itself scrolls -- "the side" on
   desktop, collapsing to a sticky bar pinned to the top on mobile (see @media below). */
.script-toc-panel {
  flex: 0 0 180px;
  position: sticky;
  top: 88px; /* clears the sticky .topbar, same offset convention as .filters */
  display: flex;
  flex-direction: column;
  gap: 14px;
  max-height: calc(100vh - 110px);
  /* Used to match --bg exactly (no border) so it wouldn't seam against the page --
     now a genuine Resting-tier surface like its sibling sidebars (.filters,
     .shot-planner-sidebar), padding and border added to match. */
  background: var(--glass-fill-resting);
  backdrop-filter: var(--glass-blur-resting);
  -webkit-backdrop-filter: var(--glass-blur-resting);
  border: 1px solid var(--glass-border-resting);
  border-radius: var(--radius-md);
  padding: 14px;
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .script-toc-panel { background: var(--surface); border-color: var(--line); }
}
.script-toc {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
.script-toc a {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-sm);
  color: var(--muted);
  text-decoration: none;
  padding: 4px 8px;
  border-radius: var(--radius-sm);
  font-family: var(--mono-script);
}
.script-toc a:hover { background: var(--surface); color: var(--ink); }
.script-toc-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}
/* "You appear in this scene" -- only ever shown to a logged-in user playing a
   character with a line somewhere in it (see routes/script.py), never a decorative
   default, so its meaning stays unambiguous. Inherits the link's own color rather
   than an accent -- it's a personal marker, not a status. */
.script-toc-mine { flex-shrink: 0; color: inherit; }
/* Has at least one note/annotation -- amber, matching the status vocabulary's own
   "worth a look" meaning elsewhere in the app, but small and understated since this
   is a passing indicator in a dense list, not a status badge. */
.script-toc-note-dot {
  flex-shrink: 0;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--amber);
  margin-left: auto;
}

.script-browse-content { flex: 1; min-width: 0; }
.script-browse-scene + .script-browse-scene { margin-top: 32px; }
/* Clears the sticky .topbar on a TOC anchor jump -- same 88px convention already
   used by .filters and .script-toc, so the target scene's heading doesn't land
   hidden underneath the header. */
.script-browse-scene { scroll-margin-top: 88px; }

.script-browse-page.hide-comments .script-annotations,
.script-browse-page.hide-comments .script-annotation-children,
.script-browse-page.hide-comments .script-note-toggle,
.script-browse-page.hide-comments .script-note-form {
  display: none !important;
}

@media (max-width: 768px) {
  .script-browse-layout { flex-direction: column; }
  .script-toc { display: none; } /* replaced by .script-toc-mobile, a dropdown */
  .script-toc-mobile { display: block; max-width: 55vw; }
  /* No side column on mobile -- the whole controls bar pins to the top of the
     viewport instead, clearing the mobile .topbar. That top offset has to match
     the topbar's own actual height, not a flat guess -- mirrors .topbar's own
     `padding-top: max(16px, env(safe-area-inset-top))` (style.css ~line 220) so
     this still clears it correctly on a notched phone, where the topbar is taller
     than its base ~77px. A flat 118px here previously left a growing gap above
     the panel once scrolled past the page's own <h1>, since a sticky element's
     top offset has nothing to do with content that happens to have scrolled away
     above it -- it only needs to clear whatever else is ALSO fixed/sticky there. */
  .script-toc-panel {
    top: calc(max(16px, env(safe-area-inset-top)) + 69px);
    flex-basis: auto;
    width: 100%;
    max-height: none;
    padding-bottom: 12px;
    /* No separate border-bottom needed anymore -- the base rule's own full border
       (now a real Resting-tier card edge) already separates this from the script
       below on every side, including this one. */
    z-index: 5;
  }
  .script-browse-controls { flex-direction: row; flex-wrap: wrap; align-items: center; gap: 10px 16px; }
}

/* ==========================================================================
   Interaction polish: toast, confirm-sheet, nav-progress, loading-button.
   See toast.js / confirm-sheet.js / nav-progress.js / loading-button.js.
   ========================================================================== */

/* A slim bar pinned to the top of the viewport for every full-page navigation
   (nav-progress.js) -- 0 width at rest, jumps to ~80% on click/submit and holds
   there; the actual navigation finishing is what makes it disappear (the whole
   DOM it lives in gets replaced), not any JS on this page. */
.nav-progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 2px;
  background: var(--ink);
  z-index: 2000;
  opacity: 0;
  transition: width 2.5s cubic-bezier(0.1, 0.6, 0.2, 1), opacity var(--fast) var(--ease);
}
.nav-progress-bar.is-active { opacity: 1; }
.nav-progress-bar.is-going { width: 80%; }

/* Bottom-anchored, one at a time -- replaces alert() for pure feedback (a failed
   save, a permission error). safe-area-inset-bottom matters here specifically:
   installed standalone on an iPhone, this sits just above the home-indicator
   strip rather than under it. */
.toast-stack {
  position: fixed;
  left: 0;
  right: 0;
  bottom: max(20px, env(safe-area-inset-bottom));
  display: flex;
  justify-content: center;
  z-index: 1500;
  pointer-events: none;
}
.toast {
  max-width: min(420px, calc(100vw - 32px));
  padding: 10px 16px;
  border-radius: var(--radius-lg);
  background: var(--glass-fill-overlay);
  backdrop-filter: var(--glass-blur-overlay);
  -webkit-backdrop-filter: var(--glass-blur-overlay);
  border: 1px solid var(--glass-border-overlay);
  box-shadow: var(--shadow-md);
  color: var(--ink);
  font-size: var(--text-base);
  text-align: center;
  cursor: pointer;
  pointer-events: auto;
  opacity: 0;
  transform: translateY(12px) scale(0.98);
  transition: opacity var(--base) var(--ease), transform var(--base) var(--ease);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .toast { background: var(--surface); }
}
.toast.is-visible { opacity: 1; transform: translateY(0) scale(1); }
.toast.is-leaving { opacity: 0; transform: translateY(6px) scale(0.98); }
.toast-error { border-color: color-mix(in srgb, var(--danger) 50%, var(--line)); color: var(--danger); }

/* iOS-action-sheet-styled bottom sheet (confirm-sheet.js), replacing confirm()
   for destructive actions -- a scrim behind it, the sheet itself slides up from
   the bottom edge (not a centered modal, which reads more like a desktop dialog
   than an app). */
.confirm-sheet-scrim {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--base) var(--ease);
  z-index: 1600;
}
.confirm-sheet-scrim.is-visible { opacity: 1; pointer-events: auto; }
.confirm-sheet {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: max(12px, env(safe-area-inset-bottom));
  max-width: 420px;
  margin: 0 auto;
  padding: 18px 16px max(16px, env(safe-area-inset-bottom));
  background: var(--glass-fill-overlay);
  backdrop-filter: var(--glass-blur-overlay);
  -webkit-backdrop-filter: var(--glass-blur-overlay);
  border: 1px solid var(--glass-border-overlay);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  display: flex;
  flex-direction: column;
  gap: 10px;
  z-index: 1601;
  opacity: 0;
  transform: translateY(24px);
  pointer-events: none;
  transition: opacity var(--base) var(--ease), transform var(--base) var(--ease);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .confirm-sheet { background: var(--surface); }
}
.confirm-sheet.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
.confirm-sheet-message { margin: 0 0 4px; text-align: center; color: var(--ink); }
.confirm-sheet-confirm, .confirm-sheet-cancel {
  width: 100%;
  padding: 12px;
  border-radius: var(--radius-md);
  font-size: var(--text-base);
  font-weight: 600;
  font-family: inherit;
  cursor: pointer;
  border: 1px solid var(--line);
  background: var(--bg);
  color: var(--danger);
}
.confirm-sheet-cancel { color: var(--ink); font-weight: 400; }
.confirm-sheet-confirm:active, .confirm-sheet-cancel:active { transform: scale(0.98); }

/* A button mid-fetch (loading-button.js) -- the label stays in the layout (so the
   button doesn't resize) but fades out under a small spinner centered on top. */
.is-loading { position: relative; color: transparent !important; pointer-events: none; }
.is-loading::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  color: var(--ink);
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
